diff --git a/.changeset/effect-rc117-repin.md b/.changeset/effect-rc117-repin.md new file mode 100644 index 000000000..0e4245fb3 --- /dev/null +++ b/.changeset/effect-rc117-repin.md @@ -0,0 +1,7 @@ +--- +'agent-bundle': patch +'@agent-bundle/runtime': patch +'create-agent-bundle': patch +--- + +Update the bundled Effect runtime to `effect@4.0.0-rc.117` (with `@effect/platform-node-shared` and `@effect/platform-node` on the same RC). Consumer installs of `agent-bundle` and `@agent-bundle/runtime` no longer pull in `msgpackr`, `msgpackr-extract`, or `fast-check` through `effect`. (#832) diff --git a/agent-patterns/effect-concurrency.md b/agent-patterns/effect-concurrency.md index a18580e99..240aa6956 100644 --- a/agent-patterns/effect-concurrency.md +++ b/agent-patterns/effect-concurrency.md @@ -14,7 +14,8 @@ lifecycle). Build those Effect-native from day one behind Promise edges. - `Fiber.await` / `Fiber.join` / `Fiber.interrupt`: observe or cancel. - Prefer exported Fiber functions over `interruptUnsafe` / `pollUnsafe`. - Forked work that owns a resource must be forked *into a scope* - (`forkScoped` / `Layer.scoped`) so interruption closes the resource. + (`forkScoped`, or a `Layer.effect` constructor) so interruption closes + the resource. Host `AbortSignal` still exists at the edges (`dispatch()` / `stream()`). Do not thread extra internal signals once the program is an Effect; interrupt diff --git a/agent-patterns/effect-errors.md b/agent-patterns/effect-errors.md index 870beb5a2..12b6d3783 100644 --- a/agent-patterns/effect-errors.md +++ b/agent-patterns/effect-errors.md @@ -65,7 +65,7 @@ export class DevCoordinatorCloseError extends YieldableFrameworkError { The bases keep the `Error` / `CodedError` constructor shapes, so migrating an existing class is the `extends` clause plus the import. They also keep the plain-`Error` observable shape, `JSON.stringify`, `stableJson`, -`{ ...error }`, `util.inspect`, non-enumerable `cause`, which rc.112 +`{ ...error }`, `util.inspect`, non-enumerable `cause`, which rc.117 `Data.Error` alone would change (its prototype `toJSON` spreads the constructor fields; its `[nodejs.util.inspect.custom]` prints that instead of the stack). Never extend `Data.Error` directly. diff --git a/agent-patterns/effect-scope.md b/agent-patterns/effect-scope.md index 162a29e17..0b9930bf9 100644 --- a/agent-patterns/effect-scope.md +++ b/agent-patterns/effect-scope.md @@ -55,9 +55,10 @@ holds the transaction open for the next caller. ## Layers -- `Layer.effect` for a service with no finalizer. -- `Layer.scoped` when constructing the service needs `Scope` (open a handle, - register a lease, start a subscriber). +- `Layer.effect` for a service, including one whose construction needs + `Scope` (open a handle, register a lease, start a subscriber): v4 has no + `Layer.scoped`; `Layer.effect` excludes `Scope` from the requirements and + closes it when the layer is released. - `Layer.effectDiscard` for a background fiber you do not expose as a service (pair with `Effect.forkScoped` / `Effect.forkChild` + scope). diff --git a/agent-patterns/effect-stream.md b/agent-patterns/effect-stream.md index 47b5cc7d6..30e839b5a 100644 --- a/agent-patterns/effect-stream.md +++ b/agent-patterns/effect-stream.md @@ -1,7 +1,7 @@ # Effect Stream patterns Source: `repos/effect/packages/effect/src/Stream.ts` (vendored v4, package -`4.0.0-rc.112`). Refresh when the subtree moves. Read +`4.0.0-rc.117`). Refresh when the subtree moves. Read `repos/effect/LLMS.md` § Working with Streams first. Stage 2 replaces the #145 pull-gated Flight `TransformStream` with Effect diff --git a/docs/effect-conventions.md b/docs/effect-conventions.md index 675d6e4d2..4efabc8e2 100644 --- a/docs/effect-conventions.md +++ b/docs/effect-conventions.md @@ -9,8 +9,8 @@ docs, or examples' user code. The four-concept newcomer ledger is untouched. | Surface | Pin | | --- | --- | -| npm `effect` | **`4.0.0-rc.112`** (exact). Latest published `rc` dist-tag on 2026-09-01. The Wave 3.5 brief named `4.0.0-rc.113`; that version was not on the registry. Re-pin chores take the next published RC. | -| Vendored tree | `repos/effect` via `git subtree` from [Effect-TS/effect](https://github.com/Effect-TS/effect.git) `main` (v4). Squash commit tracks `packages/effect` version **4.0.0-rc.112**. | +| npm `effect` | **`4.0.0-rc.117`** (exact). Latest published `rc` dist-tag on 2026-09-25 (re-pinned from `4.0.0-rc.112`, #832). Re-pin chores take the next published RC. | +| Vendored tree | `repos/effect` via `git subtree` from [Effect-TS/effect](https://github.com/Effect-TS/effect.git) at the release tag **`effect@4.0.0-rc.117`** (v4). Pull the tag matching the npm pin, not `main`: upstream `main` can carry unreleased commits under the same version. | | `website` TypeScript | Native **`7.0.2`** runs `tsc`; the `typescript-6` alias stays on **`6.0.3`** for TypeDoc, Twoslash, and `rspress.config.ts` compiler-API calls. `.pnpmfile.cjs` turns those plugins' TypeScript peers into dependencies, and the scoped overrides in `pnpm-workspace.yaml` isolate them from TypeScript 7. Re-pin chores update the alias and overrides together. | Application code imports the npm package. Never import from `repos/**`. @@ -86,7 +86,7 @@ is not the goal, and recovery keeps using `Effect.catch` + `instanceof` / What does not change: `instanceof Error` / `instanceof X`, `.name`, `.message`, `.code`, `.cause`, `.stack`, the boundary's identity-preserving rethrow (`isTypedDevError` matches the string `code`), `JSON.stringify`, -`stableJson`, `{ ...error }`, and `util.inspect`. rc.112 `Data.Error` would +`stableJson`, `{ ...error }`, and `util.inspect`. rc.117 `Data.Error` would otherwise change the last four, its prototype `toJSON` spreads the constructor fields (`message`, `cause`) into the JSON and its `[nodejs.util.inspect.custom]` prints that instead of the stack, so the @@ -169,10 +169,11 @@ on the Promise edge. Do not widen public error types to satisfy Effect. `Disposable` / `AsyncDisposable`). - Close a scope with `Effect.scoped` / `Effect.scopedWith`. Do not leak `Scope` into a public Promise signature. -- Layers compose services. `Layer.scoped` when the layer needs `Scope`. +- Layers compose services. `Layer.effect` also covers a layer that needs + `Scope` (v4 has no `Layer.scoped`). - Host `AbortSignal` at a Promise edge goes on `runPromise(..., { signal })`. Inside Effect, `interruptWhenAborted` or `yield* scopedAbortSignal`. -- `scopedAbortSignal` is `Effect.abortSignal`, which in rc.112 is exactly +- `scopedAbortSignal` is `Effect.abortSignal`, which in rc.117 is exactly `acquireRelease(sync(() => new AbortController()), c => sync(() => c.abort()))` mapped to `.signal`: it aborts only when the owning scope closes, always with no reason, and never hands out the controller. A contract that must @@ -268,7 +269,7 @@ Hurt / gotchas: `release: (a, exit) => Effect` and `Effect.addFinalizer` an `Effect`, so a release that can fail cannot even be written there. `Effect.acquireUseRelease`'s `release` and `Effect.onExit` - / `Effect.ensuring` handlers are **not** infallible: rc.112 types them + / `Effect.ensuring` handlers are **not** infallible: rc.117 types them `Effect` and merges a failing handler into the result (`combineFinalizerCause` → `Cause.combine(useCause, handlerCause)`). That merged cause is first-failure-wins once it reaches the boundary: @@ -305,7 +306,7 @@ published API surface; its dist ships inside the `agent-bundle` package as static assets. Exact-pin `effect` + `@effect/atom-react` (synchronized with the repo's effect -pin, currently `4.0.0-rc.112`) are allowed there, but only in dedicated +pin, currently `4.0.0-rc.117`) are allowed there, but only in dedicated browser-state modules (`src/runtime/agent-document-atoms.ts`, `src/routes/route-editor-atoms.ts`, and `src/discovery/discovery-atoms.ts`). The discovery module owns report loading plus ephemeral live-probe consent @@ -323,10 +324,10 @@ and result state. Atoms live in `effect/unstable/reactivity`; React bindings com - Imperative clients (`ProjectClient`, `RuntimeClient`, `AgentDocumentClient`, …) stay the lifecycle authorities; atoms are read-side caches over their decoded outputs. -- Known caveat: `4.0.0-rc.112` has a stream-backed derived-atom disposal bug - (fixed upstream post-rc.112, unpublished), no stream-backed derived atoms - in the Workbench until a re-pin past the fix; the root `RegistryProvider` - (not the default registry) avoids the reported React case. +- Stream-backed derived atoms are allowed from `4.0.0-rc.117`: the rc.112 + idle-TTL disposal bug for a stream-backed atom read through a derived atom + is fixed upstream (Effect-TS/effect#7535, first published in rc.113). Keep + the root `RegistryProvider`; the module-level default registry stays unused. - Every effect re-pin must bump `@effect/atom-react` to the same RC in the same chore, re-run the Workbench disposal regression test, and re-measure the Workbench production bundle (the rsbuild build emits the size table). @@ -336,6 +337,9 @@ and result state. Atoms live in `effect/unstable/reactivity`; React bindings com Re-evaluated 2026-09-03 against `effect@4.0.0-rc.112` + `@effect/platform-node@4.0.0-rc.112`; **decision = adopted for ordinary filesystem I/O and path operations** (the 2026-09-01 decline is superseded). +Re-verified 2026-09-25 on rc.117: `@effect/platform-node` still requires a +non-optional `redis` peer, and `NodeFileSystem` still has no `lstat`, +`O_NOFOLLOW`, or directory fsync, so the split and the keep-raw list stand. `FileSystem.FileSystem` and `Path.Path` from the `effect` package are the sanctioned way for framework code to touch the filesystem. The Node implementations come from `@effect/platform-node` (`NodeServices.layer`) in @@ -344,7 +348,7 @@ implementations come from `@effect/platform-node` (`NodeServices.layer`) in `NodeFileSystem` / `NodePath` / `NodeChildProcessSpawner` / `NodeStdio` / `NodeTerminal` / `NodeCrypto`; `@effect/platform-node`'s modules are re-exports of it) in `agent-bundle`, which every consumer installs: -`@effect/platform-node@rc.112` would add `undici`, `mime`, and, through a +`@effect/platform-node` would add `undici` (rc.112 also added `mime`) and, through a non-optional `redis` peer that npm auto-installs, a Redis client (+23 MB, +17 packages) to each consumer install. `agent-bundle`'s `platformLayer` composes the same six services the same way `NodeServices.layer` does. The @@ -374,7 +378,7 @@ the first-party CLI's user-facing text, see `try`/`finally` `rm(dir, { recursive: true, force: true })` exactly, `force`, cleanup failure as a typed `PlatformError` that wins over the operation's failure, cleanup on interruption. Not - `fs.makeTempDirectoryScoped` in library code: the rc.112 finalizer removes + `fs.makeTempDirectoryScoped` in library code: the rc.117 finalizer removes without `force` and `orDie`s, so an operation that deleted its own staging directory would fail an already successful call, and a real cleanup error would surface as the `PlatformError` wrapper (scope finalizers cannot fail @@ -508,7 +512,9 @@ import into its own chunk (`dist/scaffold-cli.js`, 447.5 kB; the argv layer is 10.8 kB), so `--help` no longer evaluates it: cold start ≈40 ms before `NodeServices.layer`, ≈65 ms with it evaluated eagerly (the 2026-09-03 regression, ≈69 ms on the 2026-09-03 re-measurement below), ≈40 ms with the -split. `undici` is not pulled into the bundle. +split. `undici` is not pulled into the bundle. Re-measured 2026-09-25: the +chunk is 449.5 kB on rc.112 and 487.4 kB on rc.117 (+37.9 kB; the package +total 462.2 kB → 500.1 kB). `packages/agent-bundle/src/effect/platform.ts` owns the framework's platform layer: `platformLayer` (the `NodeServices` union composed from @@ -566,7 +572,7 @@ place of the script's outcome. ### Terminal and Stdio: user-facing CLI text Adopted 2026-09-03 for the first-party `agent-bundle` CLI (`src/cli.ts`); the -Node implementations come from `@effect/platform-node-shared@4.0.0-rc.112`, +Node implementations come from `@effect/platform-node-shared@4.0.0-rc.117`, the same dependency `platform.ts` builds `platformLayer` from (never `@effect/platform-node`, for the consumer-footprint reason above). `effect/Terminal` is the sanctioned way to touch stdin/stdout for @@ -735,7 +741,7 @@ wire contracts](#effect-schema-wire-contracts-schema-projections). | Module | Adopted in | Re-verify | | --- | --- | --- | -| `effect/unstable/reactivity` (+ `@effect/atom-react` bindings) | Workbench Agent Document panel (#105 phase 1) and route editor (#105 phase 2) | re-pin bumps @effect/atom-react in lockstep; re-run disposal regression + bundle measurement; stream-backed derived atoms stay banned until the rc.112 disposal fix ships | +| `effect/unstable/reactivity` (+ `@effect/atom-react` bindings) | Workbench Agent Document panel (#105 phase 1) and route editor (#105 phase 2) | re-pin bumps @effect/atom-react in lockstep; re-run disposal regression + bundle measurement (rc.117: 2929.0 kB / 649.8 kB gzip total); stream-backed derived atoms allowed since rc.117 (Effect-TS/effect#7535) | | `@effect/platform-node` (`NodeServices.layer`, `create-agent-bundle`) and `@effect/platform-node-shared` (`agent-bundle`'s `platformLayer`); `FileSystem` / `Path` services live in `effect` | **adopted** (2026-09-03) for ordinary I/O, `create-agent-bundle` scaffolder and the `agent-bundle` temp directories in `api.ts` / the Codex validator (phase 1); host-contracts validators, `services/*`, `eval/*`, and the post-build readers (phase 2, ordinary-I/O modules, 2026-09-03); the dev server's services on one session-scoped runtime created in `startDevServer` (phase 2, dev server, 2026-09-03); see [Effect platform services](#effect-platform-services-effectplatform-node) for the keep-raw list and the consumer-footprint reason for the split | re-pin bumps both in lockstep with `effect`; re-check whether `@effect/platform-node` still forces a `redis` peer (if it stops, `agent-bundle` can move to `NodeServices.layer`); re-check whether `lstat` / `O_NOFOLLOW` / directory fsync landed (would shrink the keep-raw list) and the `runMain` 130/143 exit contract | | `@effect/platform-node-shared` (`NodeTerminal` / `NodeStdio`) + `effect/Terminal`, `effect/Stdio` | first-party CLI command output, diagnostics, and machine output (`src/cli.ts`, `src/effect/terminal.ts`, `src/effect/cli-runtime.ts`), loaded lazily on the first command write (2026-09-03); Commander's help/version/argv-error text and the scaffolder's `--help` / flag-error text stay on synchronous process writes for the cold-start budget | re-pin re-checks `Terminal.display` stays stdout-only, `readLine` EOF → `QuitError`, the `Stdio` sink contract, and re-measures `agent-bundle --version` startup against the recorded ≈60 ms (`cli.test.ts` fails the build if the trivial invocations resolve an `effect` module) | | `Schema` / `SchemaAST` / `SchemaParser` projections (`toType` / `toEncoded`) for wire contracts | **declined** (2026-09-01) | revisit at Effect GA or on the first encoded/decoded-divergent wire contract; re-pin re-checks the projections API and the `onExcessProperty` parse-option default | @@ -761,7 +767,10 @@ Adding the `effect` dependency changed no generated artifact, no runtime entry (`index.js`, `state.js`, `state/sqlite.js`, `plugin.js`) imports it until Stage 1+. Machine-readable copy: [effect-cold-start-baseline.json](effect-cold-start-baseline.json). Stage 2 -must not regress it: `pnpm bench:hook-cold-start -- --check`. +must not regress it: `pnpm bench:hook-cold-start -- --check`. The rc.117 +re-pin measured a 46.4–47.3 ms median (2026-09-25, Node v24.15.0 against +the baseline's v22.23.1), inside the check's budget; the baseline file was +not rewritten. ## Re-pin chore @@ -774,7 +783,10 @@ must not regress it: `pnpm bench:hook-cold-start -- --check`. the same RC; re-run the Workbench disposal regression test and production bundle measurement (rsbuild size table), and re-measure the scaffolder bundle (`pnpm --filter create-agent-bundle build` prints the size table). -3. `git subtree pull --prefix=repos/effect https://github.com/Effect-TS/effect.git main --squash`. +3. `git subtree pull --prefix=repos/effect https://github.com/Effect-TS/effect.git effect@ --squash` + (the release tag of the new pin). Fetch upstream tags in a separate clone, + never into this repository. Keep the `git-subtree-dir` / `git-subtree-split` + trailers in the PR's squash-merge body so the next pull finds its base. 4. Re-read `repos/effect/LLMS.md` and refresh `agent-patterns/effect-*.md`. 5. Re-verify every unstable-module row and the language-service diagnostics. 6. Re-run the hook cold-start check. @@ -804,7 +816,7 @@ resolved the current repo practice stands, and new code follows it. (`adapters/hook-contract.ts`, `install/surface.ts`, `dev/runtime-client-surface-proxy.ts`, `dev/mcp-apps/mcp-app-sandbox.ts`). Roughly 400 call sites in total, against about a dozen - `Predicate.isObject` uses (the install lane). rc.112 `Predicate` has no + `Predicate.isObject` uses (the install lane). rc.117 `Predicate` has no `isRecord`; `Predicate.isObject` is the closest match (`{}`-typed, excludes arrays), and `Predicate.isObjectOrArray` includes arrays. Options: declare the `core/strict-json.ts` guards the sanctioned spelling, fold the private @@ -819,7 +831,7 @@ resolved the current repo practice stands, and new code follows it. [Yieldable framework errors](#yieldable-framework-errors-dataerror-decided-2026-09-03). Summary: framework-process error classes in Effect-native modules extend `YieldableFrameworkError` / `YieldableCodedError` - (`packages/agent-bundle/src/effect/errors.ts`, thin subclasses of rc.112 + (`packages/agent-bundle/src/effect/errors.ts`, thin subclasses of rc.117 `Data.Error` that keep the `Error` / `CodedError` constructor shapes and restore plain-`Error` `toJSON` / `util.inspect`), so programs write `return yield* new X(...)`. `Schema.TaggedError` stays deferred. Plain @@ -852,6 +864,6 @@ soon as the trigger fires and retire the row. | Recorded | Pin (where) | Observed registry state | Trigger / action | | --- | --- | --- | --- | -| 2026-09-03 | `effect-rstest` **pkg.pr.new preview `e5f8d5f`** (`https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f`), `packages/agent-bundle`, `packages/rsc-runtime`, `packages/create-agent-bundle` devDependencies (three pins). Needs a real release pin once published. | `npm view effect-rstest versions`: **E404, not published to npm** (no versions, no dist-tags). | First npm publish of `effect-rstest`. Replace all three preview URLs with the exact published version, refresh `pnpm-lock.yaml`, re-run `pnpm test:unit` (`it.effect` / `it.live` suites). | -| 2026-09-03 | `effect` **`4.0.0-rc.112`** (`packages/agent-bundle`, `packages/rsc-runtime`, `packages/workbench`, `packages/create-agent-bundle`), `@effect/atom-react` `4.0.0-rc.112` (`packages/workbench`), `@effect/platform-node` `4.0.0-rc.112` (`packages/create-agent-bundle`), `@effect/platform-node-shared` `4.0.0-rc.112` (`packages/agent-bundle`), `@effect/language-service` `0.87.2` (root). `@effect/tsgo` is not part of this row: it detects the Effect major version rather than pinning an RC, and moves on its own (`0.45.0`, 2026-09-24, #817; `effect-tsgo diagnostics` output identical to `0.41.0` on the root, Workbench, and scaffolder projects). Auto re-pin in lockstep + `repos/effect` subtree + Workbench atom phase 4 unblock (stream-backed derived atoms) once the post-rc.112 disposal fix ships. | `npm view effect dist-tags`: `rc` **`4.0.0-rc.112`** (unchanged), `beta` `4.0.0-beta.107`, `latest` `3.22.1`. `@effect/atom-react`: `rc` `4.0.0-rc.112`. `@effect/language-service`: `latest` `0.87.2`. 2026-09-24: `rc` **`4.0.0-rc.117`**; the trigger has fired but the re-pin is **blocked** (#832): every `effect-rstest` build imports `effect/testing/FastCheck`, removed in rc.113 (Effect-TS/effect#7254, native Arbitraries), so six unit-test files fail to load on any RC after rc.112. | `effect-rstest` ships a build ported to `effect/unstable/arbitrary/Arbitrary` (as upstream `@effect/vitest` did); then run the re-pin chore steps 1–6 above (#832 has steps 1 and 3), bumping `effect`, `@effect/atom-react`, and `@effect/language-service` together, then lift the stream-backed derived-atom ban in the Workbench if the disposal fix is in the new RC. | +| 2026-09-25 | `effect-rstest` **pkg.pr.new preview `cbbf87e`** (`https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e`, the ScriptedAlchemy/effect-rstest#5 port to `effect/unstable/arbitrary`), `packages/agent-bundle`, `packages/rsc-runtime`, `packages/create-agent-bundle` devDependencies (three pins). Needs a real release pin once published. | ScriptedAlchemy/effect-rstest is **not published to npm**. The npm name `effect-rstest` (`latest` `0.1.5`, 2026-09-19) belongs to an unrelated port, [Nsttt/effect-rstest](https://github.com/Nsttt/effect-rstest); do not pin it by name. | First npm publish of ScriptedAlchemy/effect-rstest. Replace all three preview URLs with the exact published version, refresh `pnpm-lock.yaml`, re-run `pnpm test:unit` (`it.effect` / `it.live` / `it.prop` suites). | +| 2026-09-25 | `effect` **`4.0.0-rc.117`** (`packages/agent-bundle`, `packages/rsc-runtime`, `packages/workbench`, `packages/create-agent-bundle`), `@effect/atom-react` `4.0.0-rc.117` (`packages/workbench`), `@effect/platform-node` `4.0.0-rc.117` (`packages/create-agent-bundle`), `@effect/platform-node-shared` `4.0.0-rc.117` (`packages/agent-bundle`), `@effect/language-service` `0.87.2` (root). `@effect/tsgo` is not part of this row: it detects the Effect major version rather than pinning an RC, and moves on its own. Auto re-pin in lockstep + `repos/effect` subtree at the matching release tag. | `npm view effect dist-tags`: `rc` **`4.0.0-rc.117`**, `beta` `4.0.0-beta.107`, `latest` `3.22.2`. `@effect/atom-react`: `rc` `4.0.0-rc.117`. `@effect/language-service`: `latest` `0.87.2`. Re-pinned from rc.112 in #832 after `effect-rstest` moved off `effect/testing/FastCheck` (removed in rc.113, Effect-TS/effect#7254). | `effect@rc` advances past `4.0.0-rc.117`. Run the re-pin chore steps 1–6 above, bumping `effect`, `@effect/atom-react`, the platform packages, and `@effect/language-service` together; `effect-rstest` must build against the new RC first. | | 2026-09-03 | Agent Plugins specification **`1.0.0`**, `packages/agent-bundle/src/adapters/schemas/portable/{plugin,mcp}.schema.json` + `PROVENANCE.json` (spec repo `agentplugins/agent-plugins-spec` @ `ff8ab5e392cc87bd88d87c060815a87490e51003`, 2026-08-19), portable `adapterRevision` `1.8.0`, pins in `tests/adapter-metadata.test.ts`. Spec watch for #426; not an npm pin, so re-verify with `curl`/`gh api`, not `npm view`. | Live `https://agent-plugins.org/schemas/1.0.0/{plugin,mcp}.schema.json` rehash to the pinned sha256 (1805 / 3408 bytes). Repo `main` HEAD unchanged at the pinned commit; **no tags, no GitHub releases**. `spec/1.1.0.md` is "Status: Working Draft" (started 2026-08-15, `a2afd7ec`); in-repo `schemas/1.1.0/*.schema.json` differ from 1.0.0 only in the `$id`/`const`/`description` version strings; `https://agent-plugins.org/schemas/1.1.0/*.schema.json` → 404. Observed latest published version: **1.0.0**. | `spec/1.1.0.md` (or later) flips to "Published" **and** `agent-plugins.org/schemas//` serves both schemas. Re-pin under `schemas/portable/` with a dated `PROVENANCE.json` (sha/bytes/date/commit), bump the portable `adapterRevision`, refresh the metadata pins, run `pnpm test:unit` (portable adapter + plugin-validation suites) and `pnpm test:host-install:build`, and add a capability row per additive field. | diff --git a/packages/agent-bundle/package.json b/packages/agent-bundle/package.json index 8707ce20b..3cb8a4466 100644 --- a/packages/agent-bundle/package.json +++ b/packages/agent-bundle/package.json @@ -109,7 +109,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@effect/platform-node-shared": "4.0.0-rc.112", + "@effect/platform-node-shared": "4.0.0-rc.117", "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", @@ -123,7 +123,7 @@ "ajv-formats": "3.0.1", "chokidar": "5.0.0", "commander": "15.0.0", - "effect": "4.0.0-rc.112", + "effect": "4.0.0-rc.117", "es-module-lexer": "3.0.2", "fast-glob": "3.3.3", "ignore": "7.0.9", @@ -136,7 +136,7 @@ "@types/npm-package-arg": "6.1.4", "@types/react": "19.3.0", "@types/ws": "8.18.1", - "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f", + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e", "react": "19.3.0", "typescript-5": "npm:typescript@5.9.3", "zod": "4.6.4" diff --git a/packages/agent-bundle/src/effect/errors.ts b/packages/agent-bundle/src/effect/errors.ts index dcbd917c8..c26eae814 100644 --- a/packages/agent-bundle/src/effect/errors.ts +++ b/packages/agent-bundle/src/effect/errors.ts @@ -8,21 +8,21 @@ import { Data } from 'effect'; * behaves as the plain `Error` subclass it replaces: * * - `instanceof Error` / `instanceof X`, `.name`, `.message`, `.code`, - * `.cause`, and `.stack` are unchanged (rc.112 `Data.Error` is + * `.cause`, and `.stack` are unchanged (rc.117 `Data.Error` is * `class extends globalThis.Error`). * - `JSON.stringify(error)`, `stableJson(error)`, and `{ ...error }` stay - * byte-identical to the plain-`Error` output. rc.112 `Data.Error#toJSON` + * byte-identical to the plain-`Error` output. rc.117 `Data.Error#toJSON` * would spread the constructor fields (`message`, `cause`) into the JSON * and `stableJson` would then sort the keys; the base shadows `toJSON` so * both serializers take their plain-object path ("own enumerable fields, * insertion order"). - * - `util.inspect` / `console.error(error)` print the stack trace. rc.112 + * - `util.inspect` / `console.error(error)` print the stack trace. rc.117 * installs `[nodejs.util.inspect.custom]` on the yieldable prototype * (returning `toJSON()`), which would replace the stack with a field dump * in CLI output; the base shadows it so Node's default `Error` formatting * applies again. * - `cause` is installed exactly like `new Error(message, { cause })`: only - * when `options` carries the key, and never enumerable. rc.112 passes a + * when `options` carries the key, and never enumerable. rc.117 passes a * falsy `cause` through `Object.assign`-style property assignment, which * would make it an enumerable own field. * diff --git a/packages/agent-bundle/src/effect/platform.ts b/packages/agent-bundle/src/effect/platform.ts index 604298bdf..abf1115fe 100644 --- a/packages/agent-bundle/src/effect/platform.ts +++ b/packages/agent-bundle/src/effect/platform.ts @@ -16,10 +16,10 @@ import { runPromise, type RunPromiseOptions } from './boundary.ts'; * `NodeServices.layer`, but built from `@effect/platform-node-shared`, the * package that actually implements them (`@effect/platform-node`'s * `NodeFileSystem` etc. are re-exports). `agent-bundle` is a runtime - * dependency of every consumer, and `@effect/platform-node@rc.112` would add - * `undici`, `mime`, and — through a non-optional `redis` peer that npm - * auto-installs — a Redis client (+23 MB, +17 packages) to each install for - * a filesystem layer. `create-agent-bundle`, which bundles its + * dependency of every consumer, and `@effect/platform-node@rc.117` would add + * `undici` and — through a non-optional `redis` peer that npm auto-installs — + * a Redis client (+23 MB, +17 packages measured on rc.112) to each install + * for a filesystem layer. `create-agent-bundle`, which bundles its * dependencies, keeps `NodeServices.layer`. * * Provided at Promise edges only — the public API functions in `api.ts` and @@ -122,7 +122,7 @@ export const ensuringRemoved = ( * an interrupt cannot land between its creation and its cleanup. The * operation is restored to the caller's interruptibility before it enters * the bracket (the bracket's own mask is a no-op inside this one). Not - * `fs.makeTempDirectoryScoped`: in rc.112 its finalizer removes without + * `fs.makeTempDirectoryScoped`: through rc.117 its finalizer removes without * `force` and `orDie`s, so a missing directory would reject an already * successful call and a real cleanup error would lose its Node cause. */ diff --git a/packages/agent-bundle/tests/effect-filesystem-phase2-dev.test.ts b/packages/agent-bundle/tests/effect-filesystem-phase2-dev.test.ts index f506dd863..a77a13402 100644 --- a/packages/agent-bundle/tests/effect-filesystem-phase2-dev.test.ts +++ b/packages/agent-bundle/tests/effect-filesystem-phase2-dev.test.ts @@ -2,7 +2,7 @@ import { access, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { Effect, FileSystem, Layer, Option } from 'effect'; +import { ByteSize, Effect, FileSystem, Layer, Option } from 'effect'; import { afterEach, describe, expect, it } from '@rstest/core'; import { createDefaultRegistry } from '../src/adapters/registry.ts'; @@ -58,7 +58,7 @@ const fileInfo = (size: number): FileSystem.File.Info => ({ mtime: Option.none(), nlink: Option.none(), rdev: Option.none(), - size: FileSystem.Size(size), + size: ByteSize.bytes(size), type: 'File', uid: Option.none(), }); diff --git a/packages/agent-bundle/tests/effect-filesystem-phase2.test.ts b/packages/agent-bundle/tests/effect-filesystem-phase2.test.ts index ea91fedb7..65d9221b8 100644 --- a/packages/agent-bundle/tests/effect-filesystem-phase2.test.ts +++ b/packages/agent-bundle/tests/effect-filesystem-phase2.test.ts @@ -4,7 +4,7 @@ import { access, mkdir, mkdtemp, readFile, stat, symlink, writeFile } from 'node import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { Effect, FileSystem, Option, PlatformError } from 'effect'; +import { ByteSize, Effect, FileSystem, Option, PlatformError } from 'effect'; import { afterEach, describe, expect, it } from '@rstest/core'; import { runPromise } from '../src/effect/boundary.ts'; @@ -57,7 +57,7 @@ const fileInfo = (mode: number): FileSystem.File.Info => ({ mtime: Option.none(), nlink: Option.none(), rdev: Option.none(), - size: FileSystem.Size(3), + size: ByteSize.bytes(3), type: 'File', uid: Option.none(), }); diff --git a/packages/create-agent-bundle/package.json b/packages/create-agent-bundle/package.json index 7fb279bae..b267b5ea7 100644 --- a/packages/create-agent-bundle/package.json +++ b/packages/create-agent-bundle/package.json @@ -46,12 +46,12 @@ }, "devDependencies": { "@clack/prompts": "1.8.1", - "@effect/platform-node": "4.0.0-rc.112", + "@effect/platform-node": "4.0.0-rc.117", "@rslib/core": "1.0.0", "@rstest/core": "0.11.12", "@types/node": "26.5.1", - "effect": "4.0.0-rc.112", - "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f" + "effect": "4.0.0-rc.117", + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e" }, "peerDependencies": { "@agent-bundle/runtime": "workspace:*", diff --git a/packages/create-agent-bundle/tests/scaffold-noop.test.ts b/packages/create-agent-bundle/tests/scaffold-noop.test.ts index 90ded852c..3a25e3d08 100644 --- a/packages/create-agent-bundle/tests/scaffold-noop.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-noop.test.ts @@ -1,4 +1,4 @@ -import { Effect, FileSystem, Layer, Option, Path, PlatformError } from 'effect'; +import { ByteSize, Effect, FileSystem, Layer, Option, Path, PlatformError } from 'effect'; import { describe, expect, it } from 'effect-rstest'; import { assertLocalFrameworkTarball } from '../src/framework.ts'; @@ -25,7 +25,7 @@ const fileInfo = (type: FileSystem.File.Type): FileSystem.File.Info => ({ mtime: Option.none(), nlink: Option.none(), rdev: Option.none(), - size: FileSystem.Size(0), + size: ByteSize.bytes(0), type, uid: Option.none(), }); diff --git a/packages/rsc-runtime/package.json b/packages/rsc-runtime/package.json index b88852c55..d43fc5e94 100644 --- a/packages/rsc-runtime/package.json +++ b/packages/rsc-runtime/package.json @@ -92,7 +92,7 @@ }, "dependencies": { "@modelcontextprotocol/server": "2.0.0", - "effect": "4.0.0-rc.112", + "effect": "4.0.0-rc.117", "flare-redact": "1.6.1", "react-server-dom-rspack": "0.1.0", "rsc-markdown-stream": "workspace:^" @@ -103,7 +103,7 @@ "@rspack/core": "2.2.3", "@rstest/core": "0.11.12", "@types/react": "19.3.0", - "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f", + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e", "micromark": "4.0.2", "micromark-extension-gfm": "3.0.0", "react": "19.3.0", diff --git a/packages/workbench/package.json b/packages/workbench/package.json index 92a16ac63..aaff1a21a 100644 --- a/packages/workbench/package.json +++ b/packages/workbench/package.json @@ -13,11 +13,11 @@ "typecheck": "tsc --project tsconfig.json" }, "dependencies": { - "@effect/atom-react": "4.0.0-rc.112", + "@effect/atom-react": "4.0.0-rc.117", "@modelcontextprotocol/client": "2.0.0", "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", - "effect": "4.0.0-rc.112", + "effect": "4.0.0-rc.117", "react": "19.3.0", "react-dom": "19.3.0", "react-markdown": "10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f7f5c097..6f4ffc5ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,8 +257,8 @@ importers: specifier: workspace:* version: link:../rsc-runtime '@effect/platform-node-shared': - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112) + specifier: 4.0.0-rc.117 + version: 4.0.0-rc.117(effect@4.0.0-rc.117) '@modelcontextprotocol/client': specifier: 2.0.0 version: 2.0.0 @@ -302,8 +302,8 @@ importers: specifier: 15.0.0 version: 15.0.0 effect: - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + specifier: 4.0.0-rc.117 + version: 4.0.0-rc.117 es-module-lexer: specifier: 3.0.2 version: 3.0.2 @@ -336,8 +336,8 @@ importers: specifier: 8.18.1 version: 8.18.1 effect-rstest: - specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f - version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f(@rstest/core@0.11.12)(effect@4.0.0-rc.112) + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e(@rstest/core@0.11.12)(effect@4.0.0-rc.117) react: specifier: 19.3.0 version: 19.3.0 @@ -361,8 +361,8 @@ importers: specifier: 1.8.1 version: 1.8.1 '@effect/platform-node': - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1) + specifier: 4.0.0-rc.117 + version: 4.0.0-rc.117(effect@4.0.0-rc.117)(redis@6.2.1) '@rslib/core': specifier: 1.0.0 version: 1.0.0(typescript@7.0.2) @@ -373,11 +373,11 @@ importers: specifier: 26.5.1 version: 26.5.1 effect: - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + specifier: 4.0.0-rc.117 + version: 4.0.0-rc.117 effect-rstest: - specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f - version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f(@rstest/core@0.11.12)(effect@4.0.0-rc.112) + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e(@rstest/core@0.11.12)(effect@4.0.0-rc.117) packages/rsc-markdown-stream: devDependencies: @@ -400,8 +400,8 @@ importers: specifier: 2.0.0 version: 2.0.0 effect: - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + specifier: 4.0.0-rc.117 + version: 4.0.0-rc.117 flare-redact: specifier: 1.6.1 version: 1.6.1 @@ -428,8 +428,8 @@ importers: specifier: 19.3.0 version: 19.3.0 effect-rstest: - specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f - version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f(@rstest/core@0.11.12)(effect@4.0.0-rc.112) + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e(@rstest/core@0.11.12)(effect@4.0.0-rc.117) micromark: specifier: 4.0.2 version: 4.0.2(supports-color@7.2.0) @@ -449,8 +449,8 @@ importers: packages/workbench: dependencies: '@effect/atom-react': - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(react@19.3.0)(scheduler@0.27.0) + specifier: 4.0.0-rc.117 + version: 4.0.0-rc.117(effect@4.0.0-rc.117)(react@19.3.0)(scheduler@0.27.0) '@modelcontextprotocol/client': specifier: 2.0.0 version: 2.0.0 @@ -461,8 +461,8 @@ importers: specifier: 6.0.0 version: 6.0.0 effect: - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + specifier: 4.0.0-rc.117 + version: 4.0.0-rc.117 react: specifier: 19.3.0 version: 19.3.0 @@ -698,10 +698,10 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@effect/atom-react@4.0.0-rc.112': - resolution: {integrity: sha512-Ksf90KQa6D4UDnMj4M84arlVKNIuwxxF2GvgXBzuaYi2LsgririsUhKQ5SYaK1euf8oIYeamEuthoZSbUQmIhw==} + '@effect/atom-react@4.0.0-rc.117': + resolution: {integrity: sha512-e6swZzC+2sPY9LfICB1YQBdn6DvnNmS1p4KwppzfHjT9lv+fyDVI9IxymUQ/Oduc58rOV/4jBhZv1tu7vgx47w==} peerDependencies: - effect: ^4.0.0-rc.112 + effect: ^4.0.0-rc.117 react: '>=19.0.0 <20.0.0' scheduler: '>=0.25.0 <0.28.0' @@ -709,17 +709,17 @@ packages: resolution: {integrity: sha512-CfiSoaVQO8pZgaMZGw5VvMvRGybsJ2LaSDoLYaqiVGvo/YYPYVR2GzUKY0h1NtIweq/+SQ5XLrvtzPIttWQ0GQ==} hasBin: true - '@effect/platform-node-shared@4.0.0-rc.112': - resolution: {integrity: sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ==} + '@effect/platform-node-shared@4.0.0-rc.117': + resolution: {integrity: sha512-GyRkEfLBG49MwJe+/XDPiXYcQ44RA9p2z0AurlsWhR1cd1PqOUMEZU1pORL+/R5hR8aH6ZuqoJYD8lAc6h+wlA==} engines: {node: '>=18.0.0'} peerDependencies: - effect: ^4.0.0-rc.112 + effect: ^4.0.0-rc.117 - '@effect/platform-node@4.0.0-rc.112': - resolution: {integrity: sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ==} + '@effect/platform-node@4.0.0-rc.117': + resolution: {integrity: sha512-WbPcvQDkqXFnCeG6LohJ9eVNapCDhZbCpezhYKxm0IsZEk7swakRh7RkC1t2CXzd3hGmszTv2k9e/pXyZuL+wQ==} engines: {node: '>=18.0.0'} peerDependencies: - effect: ^4.0.0-rc.112 + effect: ^4.0.0-rc.117 redis: '>=5.0.0 <7.0.0' '@effect/tsgo-darwin-arm64@0.45.0': @@ -915,36 +915,6 @@ packages: resolution: {integrity: sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==} engines: {node: '>=20'} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} - cpu: [x64] - os: [win32] - '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: @@ -1857,10 +1827,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1871,15 +1837,15 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect-rstest@https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f: - resolution: {integrity: sha512-vAoaIALD1PEKZDHdX8RdFKzJQtV5FTTzmGt9+xSbjbCJyPX/yKXvL9kJlR9QjSXRxGwFcxy+v+QxEK2ja7JKpA==, tarball: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f} + effect-rstest@https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e: + resolution: {integrity: sha512-5+p2KfQ++ASE/jODKn7/WvTEJoLGA0EQNol/Y4yO1KsB46ZgPcwyjmS7CwmM5B7UxzB6LzbdmXmgpoE1sAfh6w==, tarball: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e} version: 0.1.0 peerDependencies: '@rstest/core': ^0.11.11 - effect: ^4.0.0-rc.108 + effect: ^4.0.0-rc.113 - effect@4.0.0-rc.112: - resolution: {integrity: sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A==} + effect@4.0.0-rc.117: + resolution: {integrity: sha512-UUyi9QiOW4nySFIBM3KnYbNMd9EZliOdKfBcyLD0mPMPheO2fkuXS5y0opjWc9oqzlJxPQICEGm8gGw3kh9j/w==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1989,10 +1955,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fast-check@4.9.0: - resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} - engines: {node: '>=12.17.0'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2557,11 +2519,6 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@4.1.0: - resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} - engines: {node: '>=16'} - hasBin: true - minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -2577,13 +2534,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.4: - resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} - hasBin: true - - msgpackr@2.1.0: - resolution: {integrity: sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ==} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2595,10 +2545,6 @@ packages: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - npm-package-arg@14.0.0: resolution: {integrity: sha512-69XQh3k+dtGa1p+7RaR57IuG3rCko96xr/nUfN4yDYBXbTYICiWcOpsFKLN2GtGE9cyIljE+f1exnaYt9MvM+Q==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -2740,9 +2686,6 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} - pure-rand@8.4.2: - resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} - qs@6.15.3: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} @@ -3154,8 +3097,8 @@ packages: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} - undici@8.10.1: - resolution: {integrity: sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q==} + undici@8.11.0: + resolution: {integrity: sha512-AdzuGcAkzhbha3GgCSoUBx2P0quwym6qdFNCQz/fDGZc8w9XJ741wHUbt2kTmfPz+ZIOMkySFkiY7RXmiDDC8w==} engines: {node: '>=22.19.0'} unhead@2.1.17: @@ -3473,30 +3416,29 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@effect/atom-react@4.0.0-rc.112(effect@4.0.0-rc.112)(react@19.3.0)(scheduler@0.27.0)': + '@effect/atom-react@4.0.0-rc.117(effect@4.0.0-rc.117)(react@19.3.0)(scheduler@0.27.0)': dependencies: - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.117 react: 19.3.0 scheduler: 0.27.0 '@effect/language-service@0.87.2': {} - '@effect/platform-node-shared@4.0.0-rc.112(effect@4.0.0-rc.112)': + '@effect/platform-node-shared@4.0.0-rc.117(effect@4.0.0-rc.117)': dependencies: '@types/ws': 8.18.1 - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.117 ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.0-rc.112(effect@4.0.0-rc.112)(redis@6.2.1)': + '@effect/platform-node@4.0.0-rc.117(effect@4.0.0-rc.117)(redis@6.2.1)': dependencies: - '@effect/platform-node-shared': 4.0.0-rc.112(effect@4.0.0-rc.112) - effect: 4.0.0-rc.112 - mime: 4.1.0 + '@effect/platform-node-shared': 4.0.0-rc.117(effect@4.0.0-rc.117) + effect: 4.0.0-rc.117 redis: 6.2.1 - undici: 8.10.1 + undici: 8.11.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -3740,24 +3682,6 @@ snapshots: '@modelcontextprotocol/core': 2.0.0 zod: 4.6.4 - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: '@emnapi/core': 1.11.3 @@ -4590,9 +4514,6 @@ snapshots: dequal@2.0.3: {} - detect-libc@2.1.2: - optional: true - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -4605,15 +4526,12 @@ snapshots: ee-first@1.1.1: {} - effect-rstest@https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f(@rstest/core@0.11.12)(effect@4.0.0-rc.112): + effect-rstest@https://pkg.pr.new/ScriptedAlchemy/effect-rstest@cbbf87e(@rstest/core@0.11.12)(effect@4.0.0-rc.117): dependencies: '@rstest/core': 0.11.12 - effect: 4.0.0-rc.112 + effect: 4.0.0-rc.117 - effect@4.0.0-rc.112: - dependencies: - fast-check: 4.9.0 - msgpackr: 2.1.0 + effect@4.0.0-rc.117: {} emoji-regex@8.0.0: {} @@ -4758,10 +4676,6 @@ snapshots: extend@3.0.2: {} - fast-check@4.9.0: - dependencies: - pure-rand: 8.4.2 - fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -5634,8 +5548,6 @@ snapshots: dependencies: mime-db: 1.54.0 - mime@4.1.0: {} - minimatch@10.2.6: dependencies: brace-expansion: 5.0.9 @@ -5646,22 +5558,6 @@ snapshots: ms@2.1.3: {} - msgpackr-extract@3.0.4: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 - optional: true - - msgpackr@2.1.0: - optionalDependencies: - msgpackr-extract: 3.0.4 - mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -5679,11 +5575,6 @@ snapshots: emojilib: 2.4.0 skin-tone: 2.0.0 - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - npm-package-arg@14.0.0: dependencies: hosted-git-info: 10.1.1 @@ -5812,8 +5703,6 @@ snapshots: punycode.js@2.3.1: {} - pure-rand@8.4.2: {} - qs@6.15.3: dependencies: es-define-property: 1.0.1 @@ -6309,7 +6198,7 @@ snapshots: undici@7.29.0: {} - undici@8.10.1: {} + undici@8.11.0: {} unhead@2.1.17: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f84740768..8d84a7147 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,7 +20,6 @@ overrides: 'typedoc>typescript': npm:typescript@6.0.3 allowBuilds: '@google/genai': false - msgpackr-extract: false protobufjs: false # LocalProvider.init() awaits FlexSearch addAsync so the first search query # cannot race the index build (#590; upstream web-infra-dev/rspress#3658). diff --git a/repos/effect/.changeset/config.json b/repos/effect/.changeset/config.json index 0a755e5f0..2b10fe8a6 100644 --- a/repos/effect/.changeset/config.json +++ b/repos/effect/.changeset/config.json @@ -17,6 +17,7 @@ "effect", "@effect/ai-anthropic", "@effect/ai-openai", + "@effect/ai-typesafe", "@effect/ai-openai-compat", "@effect/ai-openrouter", "@effect/atom-react", diff --git a/repos/effect/.changeset/exact-byte-size.md b/repos/effect/.changeset/exact-byte-size.md deleted file mode 100644 index 2442670a7..000000000 --- a/repos/effect/.changeset/exact-byte-size.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"effect": patch ---- - -Add an exact `ByteSize` value module with unambiguous decimal (`kB`, `MB`) and binary (`KiB`, `MiB`) units, parsing, formatting, checked arithmetic, and safe number conversion. Add `Schema.ByteSize*` codecs and `Config.ByteSize` for exact non-negative bigint-backed byte counts. diff --git a/repos/effect/.changeset/pre/add-schema-compilers.md b/repos/effect/.changeset/pre/add-schema-compilers.md new file mode 100644 index 000000000..5a88e849b --- /dev/null +++ b/repos/effect/.changeset/pre/add-schema-compilers.md @@ -0,0 +1,48 @@ +--- +"effect": patch +--- + +Add experimental JIT and AOT schema compilers that work through the existing +`SchemaParser` APIs and share a decoder registry. Enable JIT globally with +`effect/unstable/schema/SchemaJITCompiler/enable`, selectively with +`SchemaJITCompiler.enable(ast)`, or install generated AOT decoders without +requiring dynamic function construction. The new +`effect/unstable/schema/SchemaAOTCompiler/Build` entrypoint discovers direct +Schema exports from explicit module loaders and writes a self-installing AOT +module through Effect's `FileSystem` and `Path` services. Compiled decoders can +provide optional synchronous `decode` and `make` operations; normal +`SchemaParser` calls consume them transparently and retain `decodeEffect` and +`makeEffect` as the detailed fallbacks. AOT targets declare the operations to +prepare, so generated modules contain only those operation families and use +the interpreter if an omitted operation is later called. + +### Breaking changes + +`SchemaGetter.Getter` is now a tagged union that distinguishes synchronous, +optional, and effectful transformations. Getter values now expose only `pipe`. +Use the dual `SchemaGetter.map`, `SchemaGetter.compose`, and `SchemaGetter.run` +functions instead of the former methods. Keeping these operations standalone +lets bundlers remove composition code when an application does not use it. + +The public `Getter` constructor is removed. Use +`SchemaGetter.transformOptionalEffect` instead of `new SchemaGetter.Getter`. +`SchemaGetter.onSome` and `SchemaGetter.onNone` are also removed. Use +`transformEffect` for an effectful transformation of present values and +`transformOptionalEffect` when the transformation handles missing values. + +`Transformation#compose` is replaced by the dual standalone function +`SchemaTransformation.composeTransformation`. Replace `first.compose(second)` +with `SchemaTransformation.composeTransformation(first, second)` or +`SchemaTransformation.composeTransformation(second)(first)`. + +`SchemaTransformation.make` is renamed to +`SchemaTransformation.makeTransformation`. `SchemaTransformation.Transformation` +and `SchemaTransformation.Middleware` now implement `Pipeable`, so both values +can be passed through standalone combinators with `.pipe(...)`. + +`SchemaAST.Context.constructorDefault` now stores the constructor-default +`Effect` directly instead of wrapping it in a `SchemaAST.Link`. Constructor +defaults apply only during construction, so the direct representation avoids +giving them encoding semantics and lets construction reuse already-completed +synchronous Effects. Code that constructs or inspects `SchemaAST.Context` +should pass or read the default `Effect` directly. diff --git a/repos/effect/.changeset/pre/ai-approved-tool-results.md b/repos/effect/.changeset/pre/ai-approved-tool-results.md new file mode 100644 index 000000000..203cf0284 --- /dev/null +++ b/repos/effect/.changeset/pre/ai-approved-tool-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Retain completed tool approval results in non-streaming responses so Chat records them and does not replay approved tools on later turns. diff --git a/repos/effect/.changeset/pre/ai-decision-model.md b/repos/effect/.changeset/pre/ai-decision-model.md new file mode 100644 index 000000000..e6450a36c --- /dev/null +++ b/repos/effect/.changeset/pre/ai-decision-model.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Decision` and `DecisionModel` to `effect/unstable/ai` for batched classification, rating, and probability estimates over schema-encoded input. diff --git a/repos/effect/.changeset/align-type-id-paths.md b/repos/effect/.changeset/pre/align-type-id-paths.md similarity index 100% rename from repos/effect/.changeset/align-type-id-paths.md rename to repos/effect/.changeset/pre/align-type-id-paths.md diff --git a/repos/effect/.changeset/pre/all-union-record-channels.md b/repos/effect/.changeset/pre/all-union-record-channels.md new file mode 100644 index 000000000..ddafd3ab6 --- /dev/null +++ b/repos/effect/.changeset/pre/all-union-record-channels.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.all` to retain errors and required services from every branch of a union of record inputs. diff --git a/repos/effect/.changeset/pre/anthropic-image-strings.md b/repos/effect/.changeset/pre/anthropic-image-strings.md new file mode 100644 index 000000000..7cb1c1467 --- /dev/null +++ b/repos/effect/.changeset/pre/anthropic-image-strings.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Preserve base64 image strings in Anthropic requests. diff --git a/repos/effect/.changeset/pre/anthropic-strict-json-schema.md b/repos/effect/.changeset/pre/anthropic-strict-json-schema.md new file mode 100644 index 000000000..9a1caae53 --- /dev/null +++ b/repos/effect/.changeset/pre/anthropic-strict-json-schema.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Exclude the provider-only `strictJsonSchema` option from Anthropic Messages request bodies while preserving its control over tool strictness. diff --git a/repos/effect/.changeset/pre/anthropic-structured-output-fallback.md b/repos/effect/.changeset/pre/anthropic-structured-output-fallback.md new file mode 100644 index 000000000..3ed06de12 --- /dev/null +++ b/repos/effect/.changeset/pre/anthropic-structured-output-fallback.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix non-native structured output generation by always requesting the response tool and excluding accompanying prose when decoding its JSON payload. diff --git a/repos/effect/.changeset/pre/arbitrary-array-shrinking.md b/repos/effect/.changeset/pre/arbitrary-array-shrinking.md new file mode 100644 index 000000000..7df992f6e --- /dev/null +++ b/repos/effect/.changeset/pre/arbitrary-array-shrinking.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Add `Arbitrary.array(item, { minLength, maxLength })` for variable-length arrays of custom Arbitraries. Shrinking removes blocks of commands while preserving the remaining values and their order, and also simplifies individual elements. + +Schema-derived arrays now also try removing prefixes and interior blocks. Shrinking composed values and Schema objects preserves child candidates that were previously lost when exploring another branch. + +Shrunk results and replay paths may change from earlier native releases. Re-run affected properties to obtain new replay tokens, and preserve important failing inputs as regression tests. diff --git a/repos/effect/.changeset/pre/arbitrary-index-constraints.md b/repos/effect/.changeset/pre/arbitrary-index-constraints.md new file mode 100644 index 000000000..9cff5c7da --- /dev/null +++ b/repos/effect/.changeset/pre/arbitrary-index-constraints.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `Arbitrary.schema` to respect applicable index signatures when generating and shrinking object properties, including fixed fields in `Schema.StructWithRest` and overlapping records. + +Combine compatible string, number, and bigint constraints during generation so cases such as a `String` field constrained by a `NonEmptyString` record remain productive at size zero. Other intersections are validated and may exhaust the discard budget. diff --git a/repos/effect/.changeset/pre/array-ensure-array-elements.md b/repos/effect/.changeset/pre/array-ensure-array-elements.md new file mode 100644 index 000000000..762cc5afa --- /dev/null +++ b/repos/effect/.changeset/pre/array-ensure-array-elements.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.ArrayEnsure` to preserve array-valued element branches and outer-array encoding cardinality. diff --git a/repos/effect/.changeset/pre/atom-rpc-query-requires.md b/repos/effect/.changeset/pre/atom-rpc-query-requires.md new file mode 100644 index 000000000..7169977a4 --- /dev/null +++ b/repos/effect/.changeset/pre/atom-rpc-query-requires.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `AtomRpc.query` returning `never` for RPCs whose middleware declares service `requires`. The return-type conditional now infers all six `Rpc` type parameters, matching `mutation` and every utility in `Rpc`. diff --git a/repos/effect/.changeset/pre/atom-solid-idle-ttl.md b/repos/effect/.changeset/pre/atom-solid-idle-ttl.md new file mode 100644 index 000000000..e7a9989e5 --- /dev/null +++ b/repos/effect/.changeset/pre/atom-solid-idle-ttl.md @@ -0,0 +1,5 @@ +--- +"@effect/atom-solid": patch +--- + +Allow `RegistryProvider` to leave `defaultIdleTTL` undefined, matching the React binding and enabling immediate cleanup of unused atoms unless a TTL is explicitly configured. diff --git a/repos/effect/.changeset/pre/bound-cluster-registration-waits.md b/repos/effect/.changeset/pre/bound-cluster-registration-waits.md new file mode 100644 index 000000000..52ee07553 --- /dev/null +++ b/repos/effect/.changeset/pre/bound-cluster-registration-waits.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Bound local sends that wait for an unregistered cluster entity type. These sends now fail with an `Entity type ... not registered` defect at the shared runner registration deadline. Once that startup deadline has elapsed, sends to not-yet-registered dynamic entity types fail immediately. diff --git a/repos/effect/.changeset/bright-mimes-leave.md b/repos/effect/.changeset/pre/bright-mimes-leave.md similarity index 100% rename from repos/effect/.changeset/bright-mimes-leave.md rename to repos/effect/.changeset/pre/bright-mimes-leave.md diff --git a/repos/effect/.changeset/bright-sockets-connect.md b/repos/effect/.changeset/pre/bright-sockets-connect.md similarity index 100% rename from repos/effect/.changeset/bright-sockets-connect.md rename to repos/effect/.changeset/pre/bright-sockets-connect.md diff --git a/repos/effect/.changeset/pre/browser-crypto-missing-subtle.md b/repos/effect/.changeset/pre/browser-crypto-missing-subtle.md new file mode 100644 index 000000000..0fedd8a2b --- /dev/null +++ b/repos/effect/.changeset/pre/browser-crypto-missing-subtle.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Return a typed `PlatformError` from `BrowserCrypto` digest operations when `crypto.subtle` is unavailable, instead of failing with a defect. diff --git a/repos/effect/.changeset/pre/browser-indexeddb-binary-key-existence.md b/repos/effect/.changeset/pre/browser-indexeddb-binary-key-existence.md new file mode 100644 index 000000000..c5462e706 --- /dev/null +++ b/repos/effect/.changeset/pre/browser-indexeddb-binary-key-existence.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Fix `BrowserKeyValueStore.layerIndexedDb` to report keys containing `Uint8Array` values from `has`. diff --git a/repos/effect/.changeset/pre/browser-runtime-custom-teardown.md b/repos/effect/.changeset/pre/browser-runtime-custom-teardown.md new file mode 100644 index 000000000..443d692f7 --- /dev/null +++ b/repos/effect/.changeset/pre/browser-runtime-custom-teardown.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Invoke custom `BrowserRuntime.runMain` teardown callbacks when the main effect completes, and remove the `pagehide` listener once the main fiber finishes. diff --git a/repos/effect/.changeset/pre/bun-http-server-unix-address.md b/repos/effect/.changeset/pre/bun-http-server-unix-address.md new file mode 100644 index 000000000..b5abe92de --- /dev/null +++ b/repos/effect/.changeset/pre/bun-http-server-unix-address.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Report Unix socket addresses from Bun HTTP servers. diff --git a/repos/effect/.changeset/pre/bun-stream-preserve-failure.md b/repos/effect/.changeset/pre/bun-stream-preserve-failure.md new file mode 100644 index 000000000..c5756f3ee --- /dev/null +++ b/repos/effect/.changeset/pre/bun-stream-preserve-failure.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Preserve mapped errors from Bun readable streams. diff --git a/repos/effect/.changeset/pre/cache-invalidate-when-replacement.md b/repos/effect/.changeset/pre/cache-invalidate-when-replacement.md new file mode 100644 index 000000000..b8b861c28 --- /dev/null +++ b/repos/effect/.changeset/pre/cache-invalidate-when-replacement.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cache.invalidateWhen` and `ScopedCache.invalidateWhen` deleting a replacement entry while waiting for an earlier lookup. diff --git a/repos/effect/.changeset/pre/cache-refresh-cancellation-ownership.md b/repos/effect/.changeset/pre/cache-refresh-cancellation-ownership.md new file mode 100644 index 000000000..7ad98b91c --- /dev/null +++ b/repos/effect/.changeset/pre/cache-refresh-cancellation-ownership.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix interruption of `Cache.refresh` for a missing key removing a newer value written by `Cache.set`. diff --git a/repos/effect/.changeset/pre/cache-refresh-capacity.md b/repos/effect/.changeset/pre/cache-refresh-capacity.md new file mode 100644 index 000000000..d03ac1d50 --- /dev/null +++ b/repos/effect/.changeset/pre/cache-refresh-capacity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cache.refresh` and `ScopedCache.refresh` exceeding capacity when an existing key is evicted while its refresh is in progress. Publishing the refreshed entry now evicts older entries as needed, releasing their resources in `ScopedCache`. diff --git a/repos/effect/.changeset/pre/cache-refresh-zero-ttl-ownership.md b/repos/effect/.changeset/pre/cache-refresh-zero-ttl-ownership.md new file mode 100644 index 000000000..06f9ee7f2 --- /dev/null +++ b/repos/effect/.changeset/pre/cache-refresh-zero-ttl-ownership.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cache.refresh` for an initially missing key deleting a newer cached value when the refresh completes with zero time to live. diff --git a/repos/effect/.changeset/pre/cache-synchronous-interruption.md b/repos/effect/.changeset/pre/cache-synchronous-interruption.md new file mode 100644 index 000000000..23fde1c27 --- /dev/null +++ b/repos/effect/.changeset/pre/cache-synchronous-interruption.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `Cache` from retaining synchronously interrupted lookups. diff --git a/repos/effect/.changeset/pre/cached-with-ttl-callback.md b/repos/effect/.changeset/pre/cached-with-ttl-callback.md new file mode 100644 index 000000000..af20cb464 --- /dev/null +++ b/repos/effect/.changeset/pre/cached-with-ttl-callback.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow `Effect.cachedWithTTL` to compute the TTL from each completed `Exit`, so successes and failures can use different cache durations. diff --git a/repos/effect/.changeset/pre/calm-ducks-fail.md b/repos/effect/.changeset/pre/calm-ducks-fail.md new file mode 100644 index 000000000..7c801d321 --- /dev/null +++ b/repos/effect/.changeset/pre/calm-ducks-fail.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Wake `NodeStream.pipeThroughDuplex` readers when the upstream fails so the original error is propagated instead of hanging. diff --git a/repos/effect/.changeset/pre/calm-headers-hide.md b/repos/effect/.changeset/pre/calm-headers-hide.md new file mode 100644 index 000000000..0e3cff5d5 --- /dev/null +++ b/repos/effect/.changeset/pre/calm-headers-hide.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Respect custom HTTP header redaction when recording server span attributes. diff --git a/repos/effect/.changeset/pre/calm-olives-nest.md b/repos/effect/.changeset/pre/calm-olives-nest.md new file mode 100644 index 000000000..50f337788 --- /dev/null +++ b/repos/effect/.changeset/pre/calm-olives-nest.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-do": patch +--- + +Support nested transactions with independent child rollback. diff --git a/repos/effect/.changeset/calm-readers-transact.md b/repos/effect/.changeset/pre/calm-readers-transact.md similarity index 100% rename from repos/effect/.changeset/calm-readers-transact.md rename to repos/effect/.changeset/pre/calm-readers-transact.md diff --git a/repos/effect/.changeset/calm-trees-generate.md b/repos/effect/.changeset/pre/calm-trees-generate.md similarity index 100% rename from repos/effect/.changeset/calm-trees-generate.md rename to repos/effect/.changeset/pre/calm-trees-generate.md diff --git a/repos/effect/.changeset/pre/canonical-internet-addresses.md b/repos/effect/.changeset/pre/canonical-internet-addresses.md new file mode 100644 index 000000000..7d5cd8cc5 --- /dev/null +++ b/repos/effect/.changeset/pre/canonical-internet-addresses.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Extend `NetAddress.toCanonical` to accept internet addresses. IPv4-mapped IPv6 addresses become IPv4 while retaining the port. Other addresses retain their identity and IPv6 scope metadata. diff --git a/repos/effect/.changeset/pre/catch-stream-channel-defects.md b/repos/effect/.changeset/pre/catch-stream-channel-defects.md new file mode 100644 index 000000000..6757799f4 --- /dev/null +++ b/repos/effect/.changeset/pre/catch-stream-channel-defects.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Stream.catchDefect` and `Channel.catchDefect` for recovering from defects without catching typed failures or interruptions. diff --git a/repos/effect/.changeset/pre/channel-rundone-completion.md b/repos/effect/.changeset/pre/channel-rundone-completion.md new file mode 100644 index 000000000..c0a74a290 --- /dev/null +++ b/repos/effect/.changeset/pre/channel-rundone-completion.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Channel.runDone`; use `Channel.runDrain` to consume all output and return the completion value. diff --git a/repos/effect/.changeset/pre/child-process-astral-escape-arguments.md b/repos/effect/.changeset/pre/child-process-astral-escape-arguments.md new file mode 100644 index 000000000..f68fd0049 --- /dev/null +++ b/repos/effect/.changeset/pre/child-process-astral-escape-arguments.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve astral Unicode escapes and following arguments in `ChildProcess.make` and `ChildProcess.prefix` template literals. diff --git a/repos/effect/.changeset/pre/childprocess-process-group-wait.md b/repos/effect/.changeset/pre/childprocess-process-group-wait.md new file mode 100644 index 000000000..0a6f55a4e --- /dev/null +++ b/repos/effect/.changeset/pre/childprocess-process-group-wait.md @@ -0,0 +1,10 @@ +--- +"@effect/platform-node-shared": patch +"effect": patch +--- + +Wait for Node child process groups to exit during scoped release and `kill`. + +After signalling a process group, both operations now wait for its leader and descendants. Without `forceKillAfter`, the wait is limited to one second and never escalates. With `forceKillAfter`, the group receives `SIGKILL` at the deadline, followed by a final wait of up to one second. Native timers keep escalation working under a `TestClock`, and cleanup no longer depends on stdio closing. + +`exitCode` and `isRunning` remain tied to the leader's exit, and a leader that already exited successfully still leaves its group untouched. Process group checks count zombies, so cleanup may wait for the full bound under a non-reaping PID 1. diff --git a/repos/effect/.changeset/pre/chunk-slice-concatenation.md b/repos/effect/.changeset/pre/chunk-slice-concatenation.md new file mode 100644 index 000000000..593f5c2a6 --- /dev/null +++ b/repos/effect/.changeset/pre/chunk-slice-concatenation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Chunk` concatenation to preserve sliced elements. diff --git a/repos/effect/.changeset/clean-config-names.md b/repos/effect/.changeset/pre/clean-config-names.md similarity index 100% rename from repos/effect/.changeset/clean-config-names.md rename to repos/effect/.changeset/pre/clean-config-names.md diff --git a/repos/effect/.changeset/clean-yaks-parse.md b/repos/effect/.changeset/pre/clean-yaks-parse.md similarity index 100% rename from repos/effect/.changeset/clean-yaks-parse.md rename to repos/effect/.changeset/pre/clean-yaks-parse.md diff --git a/repos/effect/.changeset/pre/cleanup-before-use-callbacks.md b/repos/effect/.changeset/pre/cleanup-before-use-callbacks.md new file mode 100644 index 000000000..414cb54e6 --- /dev/null +++ b/repos/effect/.changeset/pre/cleanup-before-use-callbacks.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `Effect.acquireUseRelease` releases an acquired resource and `Effect.useSpan` ends its span when the use callback throws before returning an effect. The thrown exception remains a defect, but no longer skips cleanup. diff --git a/repos/effect/.changeset/pre/cleanup-successful-process-groups.md b/repos/effect/.changeset/pre/cleanup-successful-process-groups.md new file mode 100644 index 000000000..384fb153e --- /dev/null +++ b/repos/effect/.changeset/pre/cleanup-successful-process-groups.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Clean up referenced POSIX process groups on scope closure after their leader exits successfully or is terminated by a signal. diff --git a/repos/effect/.changeset/pre/clear-wings-relax.md b/repos/effect/.changeset/pre/clear-wings-relax.md new file mode 100644 index 000000000..526b3bc30 --- /dev/null +++ b/repos/effect/.changeset/pre/clear-wings-relax.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use `new` instantiation for streams diff --git a/repos/effect/.changeset/pre/cli-completion-command-aliases.md b/repos/effect/.changeset/pre/cli-completion-command-aliases.md new file mode 100644 index 000000000..00d824e98 --- /dev/null +++ b/repos/effect/.changeset/pre/cli-completion-command-aliases.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve flags and nested commands when completing a CLI subcommand through its alias. diff --git a/repos/effect/.changeset/pre/cli-completion-shared-flags.md b/repos/effect/.changeset/pre/cli-completion-shared-flags.md new file mode 100644 index 000000000..9e27bd98d --- /dev/null +++ b/repos/effect/.changeset/pre/cli-completion-shared-flags.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Include inherited shared flags in descendant CLI completions. diff --git a/repos/effect/.changeset/pre/cli-key-value-pair-first-separator.md b/repos/effect/.changeset/pre/cli-key-value-pair-first-separator.md new file mode 100644 index 000000000..d8241fd5a --- /dev/null +++ b/repos/effect/.changeset/pre/cli-key-value-pair-first-separator.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow `=` in values parsed by `Primitive.keyValuePair`, `Flag.keyValuePair`, and `Param.keyValuePair` in `effect/unstable/cli`. diff --git a/repos/effect/.changeset/pre/cli-no-color-values.md b/repos/effect/.changeset/pre/cli-no-color-values.md new file mode 100644 index 000000000..084859b63 --- /dev/null +++ b/repos/effect/.changeset/pre/cli-no-color-values.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Disable CLI formatter colors for every non-empty `NO_COLOR` value. diff --git a/repos/effect/.changeset/pre/cli-optional-alternative-flags.md b/repos/effect/.changeset/pre/cli-optional-alternative-flags.md new file mode 100644 index 000000000..743c995ff --- /dev/null +++ b/repos/effect/.changeset/pre/cli-optional-alternative-flags.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow optional alternative CLI flags. diff --git a/repos/effect/.changeset/pre/cli-scalar-constructor-names.md b/repos/effect/.changeset/pre/cli-scalar-constructor-names.md new file mode 100644 index 000000000..e5476bb8a --- /dev/null +++ b/repos/effect/.changeset/pre/cli-scalar-constructor-names.md @@ -0,0 +1,27 @@ +--- +"effect": patch +--- + +Rename CLI constructors to PascalCase, aligning scalar names with `Schema` and `Config`. This is a breaking change; parsing behavior is unchanged. + +In `Primitive`, `Param`, `Flag`, and `Argument`, capitalize existing constructor names, with these exceptions: + +| Previous | New | Modules | +| --------- | ---------- | --------------------- | +| `integer` | `Int` | All four | +| `float` | `Finite` | All four | +| `none` | `Never` | All four | +| `choice` | `Literals` | Param, Flag, Argument | + +`Primitive.choice` becomes `Primitive.Choice`; `choiceWithValue` becomes `ChoiceWithValue` where available. + +In `Prompt`, capitalize control constructors except `text` → `String`, `integer` → `Int`, and `float` → `Number`. Rename public types `IntegerOptions` → `IntOptions` and `FloatOptions` → `NumberOptions`. Shared `TextOptions` is unchanged. `Prompt.Number` retains its existing parser, without a finite-number restriction. + +In `GlobalFlag`, rename `action` → `Action` and `setting` → `Setting`. Factories and combinators, including `Command.make` and `Prompt.succeed`, keep their names. + +Update public `_tag` matches and completion descriptors: + +- `Primitive`: `"Integer"` → `"Int"`, `"Float"` → `"Finite"`, `"None"` → `"Never"`. +- `Completions.FlagType` and `Completions.ArgumentType`: `"Integer"` → `"Int"`, `"Float"` → `"Finite"`. + +Sentinels still always fail; their internal parameter name is now `"__never__"`. Help labels and completion scripts are unchanged. diff --git a/repos/effect/.changeset/pre/cli-variadic-absence-defaults.md b/repos/effect/.changeset/pre/cli-variadic-absence-defaults.md new file mode 100644 index 000000000..7f3f05e30 --- /dev/null +++ b/repos/effect/.changeset/pre/cli-variadic-absence-defaults.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix defaulted variadic arguments when omitted. diff --git a/repos/effect/.changeset/pre/cli-wizard-option-looking-values.md b/repos/effect/.changeset/pre/cli-wizard-option-looking-values.md new file mode 100644 index 000000000..852a59382 --- /dev/null +++ b/repos/effect/.changeset/pre/cli-wizard-option-looking-values.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix CLI wizard handling of negative numbers and other flag values beginning with `-`. diff --git a/repos/effect/.changeset/pre/clickhouse-compiler-dialect.md b/repos/effect/.changeset/pre/clickhouse-compiler-dialect.md new file mode 100644 index 000000000..0af5bad5e --- /dev/null +++ b/repos/effect/.changeset/pre/clickhouse-compiler-dialect.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-clickhouse": patch +--- + +Mark the ClickHouse statement compiler with the ClickHouse dialect so dialect-specific SQL fragments select the correct branch. diff --git a/repos/effect/.changeset/pre/clickhouse-ping-check.md b/repos/effect/.changeset/pre/clickhouse-ping-check.md new file mode 100644 index 000000000..0dca4a3a4 --- /dev/null +++ b/repos/effect/.changeset/pre/clickhouse-ping-check.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-clickhouse": patch +--- + +Use ClickHouse's `ping()` endpoint for connection validation and map failed health checks to `SqlError` values. diff --git a/repos/effect/.changeset/cluster-active-teardowns.md b/repos/effect/.changeset/pre/cluster-active-teardowns.md similarity index 100% rename from repos/effect/.changeset/cluster-active-teardowns.md rename to repos/effect/.changeset/pre/cluster-active-teardowns.md diff --git a/repos/effect/.changeset/pre/cluster-reply-codec-services.md b/repos/effect/.changeset/pre/cluster-reply-codec-services.md new file mode 100644 index 000000000..1ee7d1c8f --- /dev/null +++ b/repos/effect/.changeset/pre/cluster-reply-codec-services.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Reply.Reply` codecs to require client services when decoding and server services when encoding. diff --git a/repos/effect/.changeset/pre/cluster-reset-claims.md b/repos/effect/.changeset/pre/cluster-reset-claims.md new file mode 100644 index 000000000..1f8143ff5 --- /dev/null +++ b/repos/effect/.changeset/pre/cluster-reset-claims.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix cluster workflows stalling after request resets. diff --git a/repos/effect/.changeset/cold-shards-continue.md b/repos/effect/.changeset/pre/cold-shards-continue.md similarity index 100% rename from repos/effect/.changeset/cold-shards-continue.md rename to repos/effect/.changeset/pre/cold-shards-continue.md diff --git a/repos/effect/.changeset/pre/compatible-dependency-refresh.md b/repos/effect/.changeset/pre/compatible-dependency-refresh.md new file mode 100644 index 000000000..a2d211be4 --- /dev/null +++ b/repos/effect/.changeset/pre/compatible-dependency-refresh.md @@ -0,0 +1,8 @@ +--- +"@effect/platform-node": patch +"@effect/sql-libsql": patch +"@effect/sql-mysql2": patch +"@effect/doctest": patch +--- + +Update dependencies to their latest compatible versions. diff --git a/repos/effect/.changeset/pre/context-saved-getter-inference.md b/repos/effect/.changeset/pre/context-saved-getter-inference.md new file mode 100644 index 000000000..c3028f360 --- /dev/null +++ b/repos/effect/.changeset/pre/context-saved-getter-inference.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix saved curried `Context.get` calls incorrectly inferring their required service as `unknown`. diff --git a/repos/effect/.changeset/pre/cookies-error-tag.md b/repos/effect/.changeset/pre/cookies-error-tag.md new file mode 100644 index 000000000..c523bca08 --- /dev/null +++ b/repos/effect/.changeset/pre/cookies-error-tag.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the `CookiesError` tag in `effect/unstable/http` from `CookieError` to `CookiesError` to match the class name. diff --git a/repos/effect/.changeset/pre/crypto-ulid.md b/repos/effect/.changeset/pre/crypto-ulid.md new file mode 100644 index 000000000..2da81d868 --- /dev/null +++ b/repos/effect/.changeset/pre/crypto-ulid.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Crypto.randomULID` to generate ULIDs from the current `Clock` timestamp and cryptographically secure random bytes. diff --git a/repos/effect/.changeset/pre/curvy-falcons-listen.md b/repos/effect/.changeset/pre/curvy-falcons-listen.md new file mode 100644 index 000000000..5f3419fd7 --- /dev/null +++ b/repos/effect/.changeset/pre/curvy-falcons-listen.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add validated multicast address refinements for IP and MAC addresses. diff --git a/repos/effect/.changeset/pre/d1-raw-native-results.md b/repos/effect/.changeset/pre/d1-raw-native-results.md new file mode 100644 index 000000000..5441b86d1 --- /dev/null +++ b/repos/effect/.changeset/pre/d1-raw-native-results.md @@ -0,0 +1,7 @@ +--- +"@effect/sql-d1": patch +--- + +Return the complete native `D1Result` from D1 statement `.raw`, preserving `success`, `meta`, and `results` instead of returning only the row array. + +Callers that treated `.raw` as an array should read `.results` or use an ordinary or `.unprepared` statement when only rows are needed. diff --git a/repos/effect/.changeset/pre/datetime-calendar-parts.md b/repos/effect/.changeset/pre/datetime-calendar-parts.md new file mode 100644 index 000000000..5529fb6e2 --- /dev/null +++ b/repos/effect/.changeset/pre/datetime-calendar-parts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Apply DateTime calendar parts without intermediate overflow. diff --git a/repos/effect/.changeset/pre/deno-redis-url-credentials.md b/repos/effect/.changeset/pre/deno-redis-url-credentials.md new file mode 100644 index 000000000..c150c6fef --- /dev/null +++ b/repos/effect/.changeset/pre/deno-redis-url-credentials.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Decode percent-encoded Redis URL authority credentials before authentication. diff --git a/repos/effect/.changeset/pre/deno-writefile-existing-mode.md b/repos/effect/.changeset/pre/deno-writefile-existing-mode.md new file mode 100644 index 000000000..17d44f9ed --- /dev/null +++ b/repos/effect/.changeset/pre/deno-writefile-existing-mode.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Fix `FileSystem.writeFile` on Deno to preserve permissions when writing an existing file with an explicit mode. diff --git a/repos/effect/.changeset/pre/docgen-alias-signature.md b/repos/effect/.changeset/pre/docgen-alias-signature.md new file mode 100644 index 000000000..58b06047f --- /dev/null +++ b/repos/effect/.changeset/pre/docgen-alias-signature.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +Preserve alias names, constraints, and defaults in generated type signatures. diff --git a/repos/effect/.changeset/pre/docgen-class-property-examples.md b/repos/effect/.changeset/pre/docgen-class-property-examples.md new file mode 100644 index 000000000..68bfef540 --- /dev/null +++ b/repos/effect/.changeset/pre/docgen-class-property-examples.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +Type-check parsed class property examples and execute them when `runExamples` is enabled. Previously unchecked property examples with type errors now cause docgen to fail. diff --git a/repos/effect/.changeset/pre/docgen-source-relative-module-pages.md b/repos/effect/.changeset/pre/docgen-source-relative-module-pages.md new file mode 100644 index 000000000..92efd2667 --- /dev/null +++ b/repos/effect/.changeset/pre/docgen-source-relative-module-pages.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +Fix module page paths for `.`, nested, and absolute source directories so modules with the same filename in different subdirectories produce distinct pages. diff --git a/repos/effect/.changeset/pre/docgen-unique-example-files.md b/repos/effect/.changeset/pre/docgen-unique-example-files.md new file mode 100644 index 000000000..15a55b136 --- /dev/null +++ b/repos/effect/.changeset/pre/docgen-unique-example-files.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": patch +--- + +Prevent examples from modules with colliding flattened paths from overwriting each other, so all examples are checked and optionally executed. Temporary example filenames and diagnostic paths now include a numeric prefix. diff --git a/repos/effect/.changeset/pre/doctest-markdown-typescript.md b/repos/effect/.changeset/pre/doctest-markdown-typescript.md new file mode 100644 index 000000000..a42719023 --- /dev/null +++ b/repos/effect/.changeset/pre/doctest-markdown-typescript.md @@ -0,0 +1,5 @@ +--- +"@effect/doctest": patch +--- + +Fix spurious TypeScript syntax errors when running marked code fences in Markdown and MDX documents. diff --git a/repos/effect/.changeset/pre/doctest-statement-boundary.md b/repos/effect/.changeset/pre/doctest-statement-boundary.md new file mode 100644 index 000000000..9e69e68ff --- /dev/null +++ b/repos/effect/.changeset/pre/doctest-statement-boundary.md @@ -0,0 +1,5 @@ +--- +"@effect/doctest": patch +--- + +Preserve statement boundaries after generated doctest assertions. diff --git a/repos/effect/.changeset/pre/dotenv-literal-substitution.md b/repos/effect/.changeset/pre/dotenv-literal-substitution.md new file mode 100644 index 000000000..0e351277d --- /dev/null +++ b/repos/effect/.changeset/pre/dotenv-literal-substitution.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `ConfigProvider.fromDotEnvContents` variable expansion to preserve replacement tokens such as `$&` in referenced values. diff --git a/repos/effect/.changeset/pre/durable-clock-zero-threshold.md b/repos/effect/.changeset/pre/durable-clock-zero-threshold.md new file mode 100644 index 000000000..3a23956d3 --- /dev/null +++ b/repos/effect/.changeset/pre/durable-clock-zero-threshold.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `DurableClock.sleep` to preserve explicit `0` and `0n` in-memory thresholds. diff --git a/repos/effect/.changeset/pre/durable-deferred-into-encoding-services.md b/repos/effect/.changeset/pre/durable-deferred-into-encoding-services.md new file mode 100644 index 000000000..6a823609a --- /dev/null +++ b/repos/effect/.changeset/pre/durable-deferred-into-encoding-services.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Require schema encoding services when `DurableDeferred.into` records an exit. diff --git a/repos/effect/.changeset/pre/dynamic-tool-parameter-schema.md b/repos/effect/.changeset/pre/dynamic-tool-parameter-schema.md new file mode 100644 index 000000000..7d16a999d --- /dev/null +++ b/repos/effect/.changeset/pre/dynamic-tool-parameter-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update dynamic tools to advertise replacement parameter schemas after `setParameters`. diff --git a/repos/effect/.changeset/pre/eff-1004-cli-display-width.md b/repos/effect/.changeset/pre/eff-1004-cli-display-width.md new file mode 100644 index 000000000..6c0fdf593 --- /dev/null +++ b/repos/effect/.changeset/pre/eff-1004-cli-display-width.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Align CLI help tables by terminal display width for wide, emoji, combining, and zero-width graphemes. diff --git a/repos/effect/.changeset/pre/eff-1008-tool-param-failure-mode.md b/repos/effect/.changeset/pre/eff-1008-tool-param-failure-mode.md new file mode 100644 index 000000000..8939684f6 --- /dev/null +++ b/repos/effect/.changeset/pre/eff-1008-tool-param-failure-mode.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +"@effect/ai-openrouter": patch +--- + +Route tool call parameter validation failures through the tool's `failureMode` and drop `ToolParameterValidationError.toolParams`. diff --git a/repos/effect/.changeset/pre/eff-1036-fiber-allocations.md b/repos/effect/.changeset/pre/eff-1036-fiber-allocations.md new file mode 100644 index 000000000..ddd8bbc1c --- /dev/null +++ b/repos/effect/.changeset/pre/eff-1036-fiber-allocations.md @@ -0,0 +1,12 @@ +--- +"effect": patch +"@effect/opentelemetry": patch +--- + +Reduce memory usage in Effect primitives and fibers. + +Breaking: context-derived `Fiber` fields now live under `fiber.cache`. The +`currentScheduler`, `currentSpan`, `currentLogLevel`, `currentStackFrame`, and +`currentPreventYield` fields are now `scheduler`, `span`, `logLevel`, +`stackFrame`, and `preventYield`. Access `minimumLogLevel` and +`maxOpsBeforeYield` through `cache` as well. diff --git a/repos/effect/.changeset/pre/eff-1038-http-server-allocations.md b/repos/effect/.changeset/pre/eff-1038-http-server-allocations.md new file mode 100644 index 000000000..26a8ea83b --- /dev/null +++ b/repos/effect/.changeset/pre/eff-1038-http-server-allocations.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/platform-node": patch +--- + +Reduce HTTP server allocation churn when tracing is not configured and for requests that complete synchronously. diff --git a/repos/effect/.changeset/pre/eff-1039-rpc-server-allocations.md b/repos/effect/.changeset/pre/eff-1039-rpc-server-allocations.md new file mode 100644 index 000000000..5973fa3cf --- /dev/null +++ b/repos/effect/.changeset/pre/eff-1039-rpc-server-allocations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reduce per-request RPC server allocations. diff --git a/repos/effect/.changeset/pre/eff-1042-http-server-performance.md b/repos/effect/.changeset/pre/eff-1042-http-server-performance.md new file mode 100644 index 000000000..bbdd46323 --- /dev/null +++ b/repos/effect/.changeset/pre/eff-1042-http-server-performance.md @@ -0,0 +1,10 @@ +--- +"effect": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +--- + +Improve HTTP server throughput by reducing routing, request handling, response +construction, and body encoding overhead. Add `Effect.withFiberSucceed` for +synchronously computing successful values from the current fiber. Copy pooled +byte views by their exact range when exposing `ArrayBuffer` values. diff --git a/repos/effect/.changeset/pre/eff-1201-http-overhead.md b/repos/effect/.changeset/pre/eff-1201-http-overhead.md new file mode 100644 index 000000000..84500451c --- /dev/null +++ b/repos/effect/.changeset/pre/eff-1201-http-overhead.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Reduce HTTP server overhead: complete freshly created header maps in place in +`HttpServerResponse.setHeader` and `setHeaders`, compare static route prefixes +with a prepared `startsWith`, map `HttpApi` schema errors eagerly for completed +decoder results, and implement `Effect.cached` as a dedicated one-time memo +without time-to-live machinery. diff --git a/repos/effect/.changeset/eff-854-pg-connection-startup.md b/repos/effect/.changeset/pre/eff-854-pg-connection-startup.md similarity index 100% rename from repos/effect/.changeset/eff-854-pg-connection-startup.md rename to repos/effect/.changeset/pre/eff-854-pg-connection-startup.md diff --git a/repos/effect/.changeset/eff-961-node-socket-tls.md b/repos/effect/.changeset/pre/eff-961-node-socket-tls.md similarity index 100% rename from repos/effect/.changeset/eff-961-node-socket-tls.md rename to repos/effect/.changeset/pre/eff-961-node-socket-tls.md diff --git a/repos/effect/.changeset/eff-963-node-socket-server-tls.md b/repos/effect/.changeset/pre/eff-963-node-socket-server-tls.md similarity index 100% rename from repos/effect/.changeset/eff-963-node-socket-server-tls.md rename to repos/effect/.changeset/pre/eff-963-node-socket-server-tls.md diff --git a/repos/effect/.changeset/eff-965-socket-upgrade.md b/repos/effect/.changeset/pre/eff-965-socket-upgrade.md similarity index 100% rename from repos/effect/.changeset/eff-965-socket-upgrade.md rename to repos/effect/.changeset/pre/eff-965-socket-upgrade.md diff --git a/repos/effect/.changeset/eff-969-dependency-updates.md b/repos/effect/.changeset/pre/eff-969-dependency-updates.md similarity index 100% rename from repos/effect/.changeset/eff-969-dependency-updates.md rename to repos/effect/.changeset/pre/eff-969-dependency-updates.md diff --git a/repos/effect/.changeset/eff-972-deno-socket-server.md b/repos/effect/.changeset/pre/eff-972-deno-socket-server.md similarity index 100% rename from repos/effect/.changeset/eff-972-deno-socket-server.md rename to repos/effect/.changeset/pre/eff-972-deno-socket-server.md diff --git a/repos/effect/.changeset/pre/eff-997-service-keys.md b/repos/effect/.changeset/pre/eff-997-service-keys.md new file mode 100644 index 000000000..8ba1ef8c6 --- /dev/null +++ b/repos/effect/.changeset/pre/eff-997-service-keys.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize core service and runtime identities under their owning module namespaces. diff --git a/repos/effect/.changeset/pre/effect-eager-transform-arguments.md b/repos/effect/.changeset/pre/effect-eager-transform-arguments.md new file mode 100644 index 000000000..05be690d2 --- /dev/null +++ b/repos/effect/.changeset/pre/effect-eager-transform-arguments.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.fnUntracedEager` to pass the original function arguments to each transform after the current effect, matching `Effect.fn` and `Effect.fnUntraced`. diff --git a/repos/effect/.changeset/pre/effect-scoped-service-restoration.md b/repos/effect/.changeset/pre/effect-scoped-service-restoration.md new file mode 100644 index 000000000..ffc7b79ab --- /dev/null +++ b/repos/effect/.changeset/pre/effect-scoped-service-restoration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.updateServiceScoped` cleanup when an inner service provider has already completed. Closing the scope now preserves the service's absence instead of failing with a missing-service defect. diff --git a/repos/effect/.changeset/pre/effect-stream-api-alignment.md b/repos/effect/.changeset/pre/effect-stream-api-alignment.md new file mode 100644 index 000000000..dd5636e4c --- /dev/null +++ b/repos/effect/.changeset/pre/effect-stream-api-alignment.md @@ -0,0 +1,17 @@ +--- +"effect": patch +--- + +Align the `Effect` and `Stream` APIs and fix several Stream type signatures. + +- `Effect.orElseSucceed` now passes the error to the fallback function, matching `Stream.orElseSucceed`. +- `Effect.isEffect` narrows to `Effect` instead of `any`. +- `Stream.bind`, `Stream.bindEffect` and `Stream.let` allow re-binding an existing field and produce the same record type as their `Effect` counterparts. +- `Stream.Success`, `Stream.Error` and `Stream.Services` are unconstrained and distributive like the `Effect` versions. +- `Stream.partition` returns `[passes, fails]` and takes a `capacity` option, matching `Stream.partitionQueue` and `Stream.partitionEffect`. Its default capacity remains 16. +- `Stream.mapBoth` takes `onElement` / `onError`, matching `Stream.tapBoth`. +- `Stream.scan` and `Stream.scanEffect` take a lazy initial state. +- `Stream.catchTags` rejects unknown tag keys like `Effect.catchTags`. +- Fixed the data-first overloads of `Stream.runIntoPubSub` (error type was dropped), `Stream.cross` (swapped type parameter names) and `Stream.mapAccumArray` (`onHalt` return type). +- Added `Stream.as`, `Stream.tapDefect`, `Stream.tapErrorTag` and `Stream.unwrapReason`. +- Stream concurrency options use the `Types.Concurrency` alias, and JSDoc categories were consolidated across both modules. diff --git a/repos/effect/.changeset/pre/effect-unmatched-reason-preservation.md b/repos/effect/.changeset/pre/effect-unmatched-reason-preservation.md new file mode 100644 index 000000000..38f3af546 --- /dev/null +++ b/repos/effect/.changeset/pre/effect-unmatched-reason-preservation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve the original cause in `Effect.catchReason` and `Effect.catchReasons` when no nested reason matches and no fallback is provided. diff --git a/repos/effect/.changeset/pre/effectable-class-override-delegation.md b/repos/effect/.changeset/pre/effectable-class-override-delegation.md new file mode 100644 index 000000000..9a99f8e88 --- /dev/null +++ b/repos/effect/.changeset/pre/effectable-class-override-delegation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effectable.Class` evaluation by delegating to its abstract `asEffect()` method. The method is called on the instance for each execution, preserving current receiver state and provided services. diff --git a/repos/effect/.changeset/pre/effectable-mixin.md b/repos/effect/.changeset/pre/effectable-mixin.md new file mode 100644 index 000000000..b087c5cde --- /dev/null +++ b/repos/effect/.changeset/pre/effectable-mixin.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effectable.Mixin` to insert the Effect prototype into an existing class inheritance chain. The returned abstract class requires an `asEffect` method and derives its Effect type from that method through polymorphic `this`. diff --git a/repos/effect/.changeset/pre/effectify-mapper-input-tuples.md b/repos/effect/.changeset/pre/effectify-mapper-input-tuples.md new file mode 100644 index 000000000..9f6495308 --- /dev/null +++ b/repos/effect/.changeset/pre/effectify-mapper-input-tuples.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the `onError` and `onSyncError` argument tuple types in `Effect.effectify` to include only caller inputs, excluding the synthesized callback. Mapper annotations that expected a callback slot must use the caller-input tuple instead. Runtime behavior is unchanged. diff --git a/repos/effect/.changeset/pre/entity-proxy-client-codec-services.md b/repos/effect/.changeset/pre/entity-proxy-client-codec-services.md new file mode 100644 index 000000000..a7e883600 --- /dev/null +++ b/repos/effect/.changeset/pre/entity-proxy-client-codec-services.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `EntityProxyServer` handler layers to include client-side codec service requirements. diff --git a/repos/effect/.changeset/pre/entity-test-client-fatal-defect-option.md b/repos/effect/.changeset/pre/entity-test-client-fatal-defect-option.md new file mode 100644 index 000000000..403acc21d --- /dev/null +++ b/repos/effect/.changeset/pre/entity-test-client-fatal-defect-option.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor the entity layer's `disableFatalDefects` option in `Entity.makeTestClient`. When enabled, a handler defect no longer fails other pending calls to the same entity ID. The failing call still reports its defect; omitted or false options retain fatal-defect behavior. diff --git a/repos/effect/.changeset/pre/eventlog-authentication-forbidden-retry.md b/repos/effect/.changeset/pre/eventlog-authentication-forbidden-retry.md new file mode 100644 index 000000000..44b83c11b --- /dev/null +++ b/repos/effect/.changeset/pre/eventlog-authentication-forbidden-retry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Retry `EventLogRemote` writes and change streams when authentication returns `Forbidden`. diff --git a/repos/effect/.changeset/pre/eventlog-empty-chunk-framing.md b/repos/effect/.changeset/pre/eventlog-empty-chunk-framing.md new file mode 100644 index 000000000..c28935464 --- /dev/null +++ b/repos/effect/.changeset/pre/eventlog-empty-chunk-framing.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Return one empty chunk when `ChunkedMessage.split` receives an empty `Uint8Array`. diff --git a/repos/effect/.changeset/pre/exact-byte-size.md b/repos/effect/.changeset/pre/exact-byte-size.md new file mode 100644 index 000000000..d3f07145d --- /dev/null +++ b/repos/effect/.changeset/pre/exact-byte-size.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +--- + +Add `ByteSize` module and use it across the ecosystem diff --git a/repos/effect/.changeset/pre/exact-http-file-body-length.md b/repos/effect/.changeset/pre/exact-http-file-body-length.md new file mode 100644 index 000000000..72c8cd6b8 --- /dev/null +++ b/repos/effect/.changeset/pre/exact-http-file-body-length.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Calculate `HttpBody.file`, `HttpBody.fileFromInfo`, and `HttpClientRequest.bodyFile` content lengths with exact bigint arithmetic and EOF clamping. diff --git a/repos/effect/.changeset/pre/execution-plan-captured-predicate.md b/repos/effect/.changeset/pre/execution-plan-captured-predicate.md new file mode 100644 index 000000000..5dbd29881 --- /dev/null +++ b/repos/effect/.changeset/pre/execution-plan-captured-predicate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `ExecutionPlan.captureRequirements` provides captured services to effectful `while` predicates. diff --git a/repos/effect/.changeset/fair-matches-infer.md b/repos/effect/.changeset/pre/fair-matches-infer.md similarity index 100% rename from repos/effect/.changeset/fair-matches-infer.md rename to repos/effect/.changeset/pre/fair-matches-infer.md diff --git a/repos/effect/.changeset/pre/fair-workflows-suspend.md b/repos/effect/.changeset/pre/fair-workflows-suspend.md new file mode 100644 index 000000000..0c9810dea --- /dev/null +++ b/repos/effect/.changeset/pre/fair-workflows-suspend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix parallel child workflows inside activities to dispatch before suspending, release activity resources during durable waits, and resume reliably when children complete during cleanup. diff --git a/repos/effect/.changeset/pre/faster-schema-construction.md b/repos/effect/.changeset/pre/faster-schema-construction.md new file mode 100644 index 000000000..3310a7c53 --- /dev/null +++ b/repos/effect/.changeset/pre/faster-schema-construction.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Optimize schema initialization while preserving custom constructor options. diff --git a/repos/effect/.changeset/pre/fetch-raw-stream-duplex.md b/repos/effect/.changeset/pre/fetch-raw-stream-duplex.md new file mode 100644 index 000000000..ebf4b4415 --- /dev/null +++ b/repos/effect/.changeset/pre/fetch-raw-stream-duplex.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Set `duplex` for raw Web stream request bodies in `FetchHttpClient`. diff --git a/repos/effect/.changeset/pre/fiber-map-reentrant-replacement.md b/repos/effect/.changeset/pre/fiber-map-reentrant-replacement.md new file mode 100644 index 000000000..6153f0ac2 --- /dev/null +++ b/repos/effect/.changeset/pre/fiber-map-reentrant-replacement.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `FiberMap` losing track of fibers started under the same key by a replaced fiber's synchronous finalizer, ensuring they are interrupted when the map's scope closes. diff --git a/repos/effect/.changeset/pre/fiber-registration-same-fiber.md b/repos/effect/.changeset/pre/fiber-registration-same-fiber.md new file mode 100644 index 000000000..ee7c0ef2c --- /dev/null +++ b/repos/effect/.changeset/pre/fiber-registration-same-fiber.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve an already registered fiber when `FiberHandle` or `FiberMap` registers it again with `onlyIfMissing: true`, instead of interrupting it and clearing the entry. diff --git a/repos/effect/.changeset/pre/fifty-carrots-punch.md b/repos/effect/.changeset/pre/fifty-carrots-punch.md new file mode 100644 index 000000000..4ba43f52d --- /dev/null +++ b/repos/effect/.changeset/pre/fifty-carrots-punch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Exposed the platform specific pretty loggers separately. diff --git a/repos/effect/.changeset/pre/filesystem-sink-undefined-flag.md b/repos/effect/.changeset/pre/filesystem-sink-undefined-flag.md new file mode 100644 index 000000000..4c26793e9 --- /dev/null +++ b/repos/effect/.changeset/pre/filesystem-sink-undefined-flag.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `FileSystem.sink` to retain its default write flag when `flag` is undefined. diff --git a/repos/effect/.changeset/pre/filesystem-stat-optional-inode.md b/repos/effect/.changeset/pre/filesystem-stat-optional-inode.md new file mode 100644 index 000000000..6fa54f6f0 --- /dev/null +++ b/repos/effect/.changeset/pre/filesystem-stat-optional-inode.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/platform-node-shared": patch +--- + +Keep Node and Bun file stats usable when optional numeric metadata exceeds the safe integer range by returning `Option.none()` for those fields. diff --git a/repos/effect/.changeset/pre/five-spoons-visit.md b/repos/effect/.changeset/pre/five-spoons-visit.md new file mode 100644 index 000000000..f3d0b9e51 --- /dev/null +++ b/repos/effect/.changeset/pre/five-spoons-visit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Removed unused stderr option from Logger.consolePretty signature diff --git a/repos/effect/.changeset/pre/fix-atom-http-stream-success-types.md b/repos/effect/.changeset/pre/fix-atom-http-stream-success-types.md new file mode 100644 index 000000000..186f81b12 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-atom-http-stream-success-types.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Match `AtomHttpApi` query and mutation success types to the generated HTTP client, +including SSE, binary streams, and header-wrapped responses. Stream transport, +decoding, and SSE errors now appear in the stream's error channel instead of +`never`, so code that assumed a failure-free stream may need to handle them. +Runtime and serialization behavior are unchanged. diff --git a/repos/effect/.changeset/pre/fix-atom-http-top-level-dispatch.md b/repos/effect/.changeset/pre/fix-atom-http-top-level-dispatch.md new file mode 100644 index 000000000..d38505b77 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-atom-http-top-level-dispatch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `AtomHttpApi` query and mutation dispatch for top-level API groups. diff --git a/repos/effect/.changeset/pre/fix-atom-query-zero-ttl.md b/repos/effect/.changeset/pre/fix-atom-query-zero-ttl.md new file mode 100644 index 000000000..f29a0e995 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-atom-query-zero-ttl.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor explicit `timeToLive: 0` and `timeToLive: 0n` in `AtomRpc` and `AtomHttpApi` queries. Zero now opts out of the registry's default idle retention, matching other zero-duration inputs, so an unmounted query can be disposed and fetched again on remount. Omitting `timeToLive` still uses the registry default. diff --git a/repos/effect/.changeset/pre/fix-atom-rpc-client-middleware-errors.md b/repos/effect/.changeset/pre/fix-atom-rpc-client-middleware-errors.md new file mode 100644 index 000000000..882cdf455 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-atom-rpc-client-middleware-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `AtomRpc` mutation and query atoms to include client middleware errors in their result error types. diff --git a/repos/effect/.changeset/pre/fix-atom-writable-fallback.md b/repos/effect/.changeset/pre/fix-atom-writable-fallback.md new file mode 100644 index 000000000..711e5279a --- /dev/null +++ b/repos/effect/.changeset/pre/fix-atom-writable-fallback.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Forward `Atom.withFallback` writes to the primary atom. diff --git a/repos/effect/.changeset/pre/fix-bun-http-default-hostname.md b/repos/effect/.changeset/pre/fix-bun-http-default-hostname.md new file mode 100644 index 000000000..79e580325 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-bun-http-default-hostname.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-bun": patch +--- + +Fix `BunHttpServer` startup when `hostname` is omitted by explicitly listening on `::`, preserving IPv4 and IPv6 connectivity and avoiding a `ServeError` when parsing Bun's default hostname. + +Preserve the configured listen address in `BunClusterHttp`, including loopback addresses, when applying the HTTP server hostname default. diff --git a/repos/effect/.changeset/pre/fix-cors-vary.md b/repos/effect/.changeset/pre/fix-cors-vary.md new file mode 100644 index 000000000..b5c0fe58b --- /dev/null +++ b/repos/effect/.changeset/pre/fix-cors-vary.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpMiddleware.cors` to preserve `Origin` and other required `Vary` dimensions. diff --git a/repos/effect/.changeset/pre/fix-dangling-internal-declarations.md b/repos/effect/.changeset/pre/fix-dangling-internal-declarations.md new file mode 100644 index 000000000..5fb1a0e7c --- /dev/null +++ b/repos/effect/.changeset/pre/fix-dangling-internal-declarations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix published declarations referencing symbols stripped as `@internal`, which broke consumers compiling with `skipLibCheck: false`. `Effectable.d.ts` now uses the public `Effect.TypeId`, `Match.d.ts` no longer aliases an internal `Contextual` type, `Schema.d.ts` ships the `AnnotationSchemaConstraint` alias it references, and the CLI's `toFlagDoc` helper is marked internal so it no longer leaks `Param.getParamMetadata`. diff --git a/repos/effect/.changeset/pre/fix-deep-mutable-opaque-types.md b/repos/effect/.changeset/pre/fix-deep-mutable-opaque-types.md new file mode 100644 index 000000000..0e0efdbac --- /dev/null +++ b/repos/effect/.changeset/pre/fix-deep-mutable-opaque-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Types.DeepMutable` to preserve built-in objects, Effect data types, and other objects with methods or symbol-keyed properties while recursively making arrays, tuples, maps, sets, and plain records mutable. diff --git a/repos/effect/.changeset/pre/fix-deferred-handover-rpc-interruption.md b/repos/effect/.changeset/pre/fix-deferred-handover-rpc-interruption.md new file mode 100644 index 000000000..d8fba8505 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-deferred-handover-rpc-interruption.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Retain durable deferred completions received before a workflow owner's first local run so replay can complete while the deferred reply is still being persisted. + +Keep pending completions in a cache keyed weakly by cluster activation. Handler rebuilds retain completions, and overlapping activations cannot clear each other's results. Results can be collected once their activation scope becomes unreachable; closing a scope alone does not guarantee collection. + +Release RPC stream and queue consumers when their request write fiber is interrupted. diff --git a/repos/effect/.changeset/pre/fix-discarded-workflow-deferred-wake.md b/repos/effect/.changeset/pre/fix-discarded-workflow-deferred-wake.md new file mode 100644 index 000000000..8fad39371 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-discarded-workflow-deferred-wake.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix lost durable deferred wake-ups when a ClusterWorkflowEngine execution suspends before its run reply is persisted. Deferred completions now wait for the current run reply before resuming, so discarded executions can replay without relying on caller retries. diff --git a/repos/effect/.changeset/pre/fix-encoded-header-metadata.md b/repos/effect/.changeset/pre/fix-encoded-header-metadata.md new file mode 100644 index 000000000..d44380628 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-encoded-header-metadata.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use body status and encoding defaults in `HttpApiSchema.encodeToWithHeaders`. diff --git a/repos/effect/.changeset/pre/fix-file-response-content-type.md b/repos/effect/.changeset/pre/fix-file-response-content-type.md new file mode 100644 index 000000000..6f8badace --- /dev/null +++ b/repos/effect/.changeset/pre/fix-file-response-content-type.md @@ -0,0 +1,11 @@ +--- +"effect": patch +"@effect/platform-node": patch +"@effect/platform-deno": patch +--- + +Fix file response content types: honor the `contentType` option and preserve explicit headers, including MIME types set by `HttpStaticServer`. The default `HttpPlatform.layer` now infers missing content types from file extensions. + +Web file responses on the default, Node, and Deno platforms prefer explicit content types, then nonempty `File.type`, then the file extension. + +Removed the unused `contentLength` option from `HttpServerResponse.file`; lengths are calculated from the file and requested range. diff --git a/repos/effect/.changeset/pre/fix-filesystem-integer-precision.md b/repos/effect/.changeset/pre/fix-filesystem-integer-precision.md new file mode 100644 index 000000000..70e684da5 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-filesystem-integer-precision.md @@ -0,0 +1,9 @@ +--- +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +"effect": patch +--- + +Prevent precision loss in Node/Bun filesystem operations and `HttpPlatform` file responses. diff --git a/repos/effect/.changeset/pre/fix-http-platform-file-ranges.md b/repos/effect/.changeset/pre/fix-http-platform-file-ranges.md new file mode 100644 index 000000000..87b522ab5 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-http-platform-file-ranges.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Clamp HttpPlatform file response ranges to the file size so Content-Length matches the bytes available. Oversized reads stop at EOF, and offsets at or past EOF return an empty body with Content-Length 0. Apply the same clamping to the default Web file response implementation. diff --git a/repos/effect/.changeset/pre/fix-http-toweb-null-body-status.md b/repos/effect/.changeset/pre/fix-http-toweb-null-body-status.md new file mode 100644 index 000000000..dccd82337 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-http-toweb-null-body-status.md @@ -0,0 +1,7 @@ +--- +"effect": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +--- + +Omit response bodies for statuses 204, 205, and 304 in `HttpServerResponse.toWeb` and the Bun/Deno HTTP adapters, preventing invalid Web responses and hung requests. Cancel omitted raw `ReadableStream` bodies, and finalize request resources without starting omitted Effect streams. diff --git a/repos/effect/.changeset/pre/fix-httpapi-form-responses.md b/repos/effect/.changeset/pre/fix-httpapi-form-responses.md new file mode 100644 index 000000000..9e9f6f333 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-httpapi-form-responses.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiClient` decoding form-urlencoded responses. diff --git a/repos/effect/.changeset/pre/fix-httpapi-test-pre-response-handlers.md b/repos/effect/.changeset/pre/fix-httpapi-test-pre-response-handlers.md new file mode 100644 index 000000000..d1b399851 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-httpapi-test-pre-response-handlers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Run registered pre-response handlers before `HttpApiTest` returns responses. diff --git a/repos/effect/.changeset/pre/fix-httpapi-url-builder-base-path.md b/repos/effect/.changeset/pre/fix-httpapi-url-builder-base-path.md new file mode 100644 index 000000000..9733d4db0 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-httpapi-url-builder-base-path.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiClient.urlBuilder` dropping base URL pathnames. diff --git a/repos/effect/.changeset/pre/fix-json-schema-percent-references.md b/repos/effect/.changeset/pre/fix-json-schema-percent-references.md new file mode 100644 index 000000000..01c6a3c47 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-json-schema-percent-references.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `JsonPointer.parseUriFragment` and `JsonPointer.formatUriFragment` for converting RFC 6901 URI fragments, and use them to preserve percent-encoded definition names in exported JSON Schema references. JSON Schema compilation now rejects malformed local definition references returned by `toJsonSchema` hooks. Such hooks must percent-encode characters that URI fragments do not permit, for example `%` as `%25` and `#` as `%23`. diff --git a/repos/effect/.changeset/pre/fix-memory-workflow-self-completion.md b/repos/effect/.changeset/pre/fix-memory-workflow-self-completion.md new file mode 100644 index 000000000..aaf1f81c5 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-memory-workflow-self-completion.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix a deadlock in the memory workflow engine when a durable deferred is completed from a finalizer in the workflow awaiting it, including `DurableDeferred.into` inside `DurableDeferred.raceAll`. diff --git a/repos/effect/.changeset/pre/fix-mime-parameter-normalization.md b/repos/effect/.changeset/pre/fix-mime-parameter-normalization.md new file mode 100644 index 000000000..545607aac --- /dev/null +++ b/repos/effect/.changeset/pre/fix-mime-parameter-normalization.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize MIME type parameters and whitespace in `Mime.getAllExtensions`. diff --git a/repos/effect/.changeset/pre/fix-multipart-active-file-failure.md b/repos/effect/.changeset/pre/fix-multipart-active-file-failure.md new file mode 100644 index 000000000..bff8687e1 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-multipart-active-file-failure.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix multipart file streams hanging on body read errors and preserve error causes when persisting files. diff --git a/repos/effect/.changeset/pre/fix-negative-file-seek.md b/repos/effect/.changeset/pre/fix-negative-file-seek.md new file mode 100644 index 000000000..50227499b --- /dev/null +++ b/repos/effect/.changeset/pre/fix-negative-file-seek.md @@ -0,0 +1,7 @@ +--- +"effect": patch +"@effect/platform-node-shared": patch +"@effect/platform-deno": patch +--- + +On Node and Deno, `FileSystem.File.seek` now rejects negative resulting positions with a `BadArgument` platform error, leaving the cursor unchanged. Its return type is now `Effect`. diff --git a/repos/effect/.changeset/pre/fix-node-compression-content-type.md b/repos/effect/.changeset/pre/fix-node-compression-content-type.md new file mode 100644 index 000000000..eef388ec4 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-node-compression-content-type.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/platform-node": patch +"@effect/platform-node-shared": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +--- + +Preserve the response Content-Type when compressing file, raw, stream, and byte-array responses on Node, Bun, Deno, and the web platform, including headers overridden after body construction. diff --git a/repos/effect/.changeset/pre/fix-openapi-client-form-url-encoding.md b/repos/effect/.changeset/pre/fix-openapi-client-form-url-encoding.md new file mode 100644 index 000000000..99c7fd034 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-openapi-client-form-url-encoding.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Encode form-urlencoded request bodies in generated HTTP clients. diff --git a/repos/effect/.changeset/pre/fix-openapi-client-multipart-records.md b/repos/effect/.changeset/pre/fix-openapi-client-multipart-records.md new file mode 100644 index 000000000..c4c36d038 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-openapi-client-multipart-records.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Encode multipart record payloads as form data in generated schema-backed clients. diff --git a/repos/effect/.changeset/pre/fix-openapi-client-paths.md b/repos/effect/.changeset/pre/fix-openapi-client-paths.md new file mode 100644 index 000000000..04b3c50ec --- /dev/null +++ b/repos/effect/.changeset/pre/fix-openapi-client-paths.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Quote static paths and percent-encode path parameters in generated HTTP clients, including SSE and binary stream methods. Reject empty parameters and dot segments with an HTTP client error before sending a request. diff --git a/repos/effect/.changeset/pre/fix-openapi-endpoint-transform-order.md b/repos/effect/.changeset/pre/fix-openapi-endpoint-transform-order.md new file mode 100644 index 000000000..ff1335312 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-openapi-endpoint-transform-order.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Apply endpoint OpenAPI overrides and transforms after schema generation. diff --git a/repos/effect/.changeset/pre/fix-openapi-stream-transform-client.md b/repos/effect/.changeset/pre/fix-openapi-stream-transform-client.md new file mode 100644 index 000000000..13f781b60 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-openapi-stream-transform-client.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Apply `transformClient` when generated binary and SSE streams are consumed, preserving lazy stream construction. diff --git a/repos/effect/.changeset/pre/fix-pg-tls-servername.md b/repos/effect/.changeset/pre/fix-pg-tls-servername.md new file mode 100644 index 000000000..258137c25 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-pg-tls-servername.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Default TLS servername to the hostname for non-IP hosts during connection startup and cancellation, restoring connections to Postgres endpoints that require SNI. Preserve explicit SSL servername overrides. diff --git a/repos/effect/.changeset/pre/fix-prompt-date-tab-buffer.md b/repos/effect/.changeset/pre/fix-prompt-date-tab-buffer.md new file mode 100644 index 000000000..43f07df7a --- /dev/null +++ b/repos/effect/.changeset/pre/fix-prompt-date-tab-buffer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Prompt.date` carrying typed digits into the next field when pressing Tab, including when navigation wraps. diff --git a/repos/effect/.changeset/pre/fix-raw-response-bodyless-headers.md b/repos/effect/.changeset/pre/fix-raw-response-bodyless-headers.md new file mode 100644 index 000000000..422cba3fe --- /dev/null +++ b/repos/effect/.changeset/pre/fix-raw-response-bodyless-headers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +`HttpServerResponse.toWeb` now keeps a raw `Response`'s headers and `Set-Cookie` values when the body is omitted (HEAD, or status 204, 205 or 304). Bodyless outer statuses keep the outer status and status text; HEAD keeps the raw `Response`'s. Outer cookies are appended to native `Set-Cookie` headers instead of replacing them. diff --git a/repos/effect/.changeset/pre/fix-rcref-idle-ttl-zero.md b/repos/effect/.changeset/pre/fix-rcref-idle-ttl-zero.md new file mode 100644 index 000000000..e37e347c3 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-rcref-idle-ttl-zero.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RcRef.make` to treat `idleTimeToLive: 0` like `Duration.zero` and `"0 millis"` instead of an omitted option. diff --git a/repos/effect/.changeset/pre/fix-react-ref-switch.md b/repos/effect/.changeset/pre/fix-react-ref-switch.md new file mode 100644 index 000000000..0044f5227 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-react-ref-switch.md @@ -0,0 +1,5 @@ +--- +"@effect/atom-react": patch +--- + +Ensure `useAtomRef` updates after switching refs when the new value matches the previous ref's value. diff --git a/repos/effect/.changeset/pre/fix-reactivity-duplicate-keys.md b/repos/effect/.changeset/pre/fix-reactivity-duplicate-keys.md new file mode 100644 index 000000000..2f0111087 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-reactivity-duplicate-keys.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `Reactivity.query` cleanup from failing when keys are repeated. diff --git a/repos/effect/.changeset/pre/fix-read-alloc-invalid-sizes.md b/repos/effect/.changeset/pre/fix-read-alloc-invalid-sizes.md new file mode 100644 index 000000000..b656ca94c --- /dev/null +++ b/repos/effect/.changeset/pre/fix-read-alloc-invalid-sizes.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-node-shared": patch +"@effect/platform-deno": patch +--- + +Fix `File.readAlloc` on Node and Deno to fail with `PlatformError` (`BadArgument`) for negative, fractional, non-finite, or unallocatable sizes without moving the cursor. Zero-size reads continue to return `Option.none()` without moving the cursor. diff --git a/repos/effect/.changeset/pre/fix-redacted-transform-results.md b/repos/effect/.changeset/pre/fix-redacted-transform-results.md new file mode 100644 index 000000000..97385788c --- /dev/null +++ b/repos/effect/.changeset/pre/fix-redacted-transform-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.Redacted` to wrap the transformed result of its inner schema during decoding and encoding. diff --git a/repos/effect/.changeset/pre/fix-runner-stream-disconnect.md b/repos/effect/.changeset/pre/fix-runner-stream-disconnect.md new file mode 100644 index 000000000..6a72f5da6 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-runner-stream-disconnect.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Release non-persisted, interruptible `RunnerServer` handlers and mailbox slots when callers disconnect. + +Preserve dynamic `WithTransaction` annotations during replay and re-delivery. diff --git a/repos/effect/.changeset/pre/fix-schema-codegen-optional-tuples.md b/repos/effect/.changeset/pre/fix-schema-codegen-optional-tuples.md new file mode 100644 index 000000000..6a39c5f2c --- /dev/null +++ b/repos/effect/.changeset/pre/fix-schema-codegen-optional-tuples.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `SchemaRepresentation.toCodeDocument` generating invalid TypeScript for optional tuple elements containing unions or nested readonly tuples. Optional element types are now parenthesized, for example `readonly [(string | number)?]` instead of `readonly [string | number?]`. Generated runtime schemas are unchanged. diff --git a/repos/effect/.changeset/pre/fix-schema-codegen-proto.md b/repos/effect/.changeset/pre/fix-schema-codegen-proto.md new file mode 100644 index 000000000..38f773bc3 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-schema-codegen-proto.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `SchemaRepresentation.toCodeDocument` dropping Struct fields named `__proto__` from generated schemas. These fields now use computed keys, such as `Schema.Struct({ ["__proto__"]: Schema.String })`, so the generated schema validates them correctly. diff --git a/repos/effect/.changeset/pre/fix-sink-flatmap-leftovers.md b/repos/effect/.changeset/pre/fix-sink-flatmap-leftovers.md new file mode 100644 index 000000000..0afe46e74 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-sink-flatmap-leftovers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve pending leftovers when a `Sink.flatMap` continuation completes without consuming input. diff --git a/repos/effect/.changeset/pre/fix-sse-mixed-line-endings.md b/repos/effect/.changeset/pre/fix-sse-mixed-line-endings.md new file mode 100644 index 000000000..c1b272729 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-sse-mixed-line-endings.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve SSE events with mixed line endings. diff --git a/repos/effect/.changeset/pre/fix-static-head-range.md b/repos/effect/.changeset/pre/fix-static-head-range.md new file mode 100644 index 000000000..915c8deb8 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-static-head-range.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ignore Range headers on non-GET requests in HttpStaticServer. diff --git a/repos/effect/.changeset/pre/fix-static-oversized-ranges.md b/repos/effect/.changeset/pre/fix-static-oversized-ranges.md new file mode 100644 index 000000000..583891479 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-static-oversized-ranges.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Parse HttpStaticServer byte range integers exactly, including values above Number.MAX_SAFE_INTEGER. Oversized starts now return 416 with Content-Range instead of falling back to 200. Oversized ends clamp to the last byte, and oversized suffixes return the whole file as 206. diff --git a/repos/effect/.changeset/pre/fix-stream-wrapper-status.md b/repos/effect/.changeset/pre/fix-stream-wrapper-status.md new file mode 100644 index 000000000..9568a5cfd --- /dev/null +++ b/repos/effect/.changeset/pre/fix-stream-wrapper-status.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiBuilder` ignoring the status annotation on a `HttpApiSchema.WithHeaders` wrapper around a streaming success, which defected when the wrapper and inner statuses differed. diff --git a/repos/effect/.changeset/pre/fix-toml-array-subtables.md b/repos/effect/.changeset/pre/fix-toml-array-subtables.md new file mode 100644 index 000000000..07d3edbae --- /dev/null +++ b/repos/effect/.changeset/pre/fix-toml-array-subtables.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Toml.parse` rejecting child tables in separate array-of-tables entries. diff --git a/repos/effect/.changeset/pre/fix-tool-result-serialization.md b/repos/effect/.changeset/pre/fix-tool-result-serialization.md new file mode 100644 index 000000000..65accd0f1 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-tool-result-serialization.md @@ -0,0 +1,13 @@ +--- +"effect": patch +--- + +Fix tool result serialization to select the codec using `isFailure` and preserve `encodedResult` through `Response.AllParts` round trips. + +Add `Tool.failureResultSchema(tool)` and `Tool.ExecutionFailure` to handle user failures, `AiError`, and denied or interrupted calls consistently. Also export `HttpRequestDetails` and `HttpResponseDetails` from `AiError`; the `Response` exports remain available. + +### Breaking changes + +- Stored results must match the selected schema. With success `Schema.Number` and failure `Schema.NumberFromString`, migrate failed results from `404` to `"404"`. +- `Response.ToolResultPart` returns `Schema.Codec` instead of `Schema.decodeTo`. Update annotations that depend on the old type. +- `Tool.FailureResult` and `Tool.Result`, including their encoded variants, now include `Tool.ExecutionFailure` in both failure modes. Handle it when narrowing failed results. diff --git a/repos/effect/.changeset/pre/fix-vue-ref-switch.md b/repos/effect/.changeset/pre/fix-vue-ref-switch.md new file mode 100644 index 000000000..b804de834 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-vue-ref-switch.md @@ -0,0 +1,5 @@ +--- +"@effect/atom-vue": patch +--- + +Fix `useAtomRef` returning a stale value after switching refs. diff --git a/repos/effect/.changeset/pre/fix-workflow-activity-interruption.md b/repos/effect/.changeset/pre/fix-workflow-activity-interruption.md new file mode 100644 index 000000000..2ce3cda65 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-workflow-activity-interruption.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix activity count leaks when workflow activity acquisition is interrupted, which could block later workflow suspension. diff --git a/repos/effect/.changeset/pre/fix-yaml-folded-scalars.md b/repos/effect/.changeset/pre/fix-yaml-folded-scalars.md new file mode 100644 index 000000000..eeb3dfef2 --- /dev/null +++ b/repos/effect/.changeset/pre/fix-yaml-folded-scalars.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix folded YAML scalars to preserve paragraph and indentation breaks. diff --git a/repos/effect/.changeset/flat-pipelines-share.md b/repos/effect/.changeset/pre/flat-pipelines-share.md similarity index 100% rename from repos/effect/.changeset/flat-pipelines-share.md rename to repos/effect/.changeset/pre/flat-pipelines-share.md diff --git a/repos/effect/.changeset/pre/formatter-defined-error-causes.md b/repos/effect/.changeset/pre/formatter-defined-error-causes.md new file mode 100644 index 000000000..51f66c0f4 --- /dev/null +++ b/repos/effect/.changeset/pre/formatter-defined-error-causes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve defined falsy Error causes (`0`, `false`, `""`, `null`, `0n`, and `NaN`) in `Formatter.format` output. Missing and explicitly `undefined` causes remain omitted. diff --git a/repos/effect/.changeset/pre/fresh-pools-release.md b/repos/effect/.changeset/pre/fresh-pools-release.md new file mode 100644 index 000000000..0e4248826 --- /dev/null +++ b/repos/effect/.changeset/pre/fresh-pools-release.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix memory leaks in `Pool.makeWithTTL` with the usage strategy by releasing retired resources and consumed acquisition errors from the TTL queue. diff --git a/repos/effect/.changeset/fresh-pools-rotate.md b/repos/effect/.changeset/pre/fresh-pools-rotate.md similarity index 100% rename from repos/effect/.changeset/fresh-pools-rotate.md rename to repos/effect/.changeset/pre/fresh-pools-rotate.md diff --git a/repos/effect/.changeset/fuzzy-classes-compare.md b/repos/effect/.changeset/pre/fuzzy-classes-compare.md similarity index 100% rename from repos/effect/.changeset/fuzzy-classes-compare.md rename to repos/effect/.changeset/pre/fuzzy-classes-compare.md diff --git a/repos/effect/.changeset/pre/graph-bellman-ford-infinite-cycle-barriers.md b/repos/effect/.changeset/pre/graph-bellman-ford-infinite-cycle-barriers.md new file mode 100644 index 000000000..e016e3903 --- /dev/null +++ b/repos/effect/.changeset/pre/graph-bellman-ford-infinite-cycle-barriers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.bellmanFord` reporting a negative cycle as affecting a target across an impassable, positive-infinite-weight edge. Targets separated from the cycle by such edges now retain their finite shortest path or remain unreachable, while targets reachable from the cycle through finite-weight edges still report an error. diff --git a/repos/effect/.changeset/pre/hashmap-collision-entries.md b/repos/effect/.changeset/pre/hashmap-collision-entries.md new file mode 100644 index 000000000..fa077d00b --- /dev/null +++ b/repos/effect/.changeset/pre/hashmap-collision-entries.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `HashMap` iterators from exposing mutable internal collision entries. diff --git a/repos/effect/.changeset/pre/hashring-exclusion-endpoint.md b/repos/effect/.changeset/pre/hashring-exclusion-endpoint.md new file mode 100644 index 000000000..3e54251e1 --- /dev/null +++ b/repos/effect/.changeset/pre/hashring-exclusion-endpoint.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HashRing.getShards` skipping an eligible node at the first ring position when other nodes have reached their allocation quota. diff --git a/repos/effect/.changeset/pre/headers-redacted-name-case.md b/repos/effect/.changeset/pre/headers-redacted-name-case.md new file mode 100644 index 000000000..70d6358da --- /dev/null +++ b/repos/effect/.changeset/pre/headers-redacted-name-case.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Compare header names case-insensitively in `Headers.isRedactedName`. diff --git a/repos/effect/.changeset/pre/headers-stateful-patterns.md b/repos/effect/.changeset/pre/headers-stateful-patterns.md new file mode 100644 index 000000000..db5ebef9e --- /dev/null +++ b/repos/effect/.changeset/pre/headers-stateful-patterns.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Headers.redact` and `Headers.isRedactedName` skipping matches when a redaction pattern is a global or sticky regular expression. diff --git a/repos/effect/.changeset/pre/http-client-catch-response-result.md b/repos/effect/.changeset/pre/http-client-catch-response-result.md new file mode 100644 index 000000000..29bd409fb --- /dev/null +++ b/repos/effect/.changeset/pre/http-client-catch-response-result.md @@ -0,0 +1,8 @@ +--- +"effect": patch +--- + +Constrain the data-first `HttpClient.catch(client, recover)` overload to recover with +`HttpClientResponse` values, matching the data-last overload. Callbacks returning other +success types are now rejected; use `Effect.catch` on the result of `client.execute(request)` +to recover to arbitrary values. diff --git a/repos/effect/.changeset/pre/http-client-redirect-preprocessing-recovery.md b/repos/effect/.changeset/pre/http-client-redirect-preprocessing-recovery.md new file mode 100644 index 000000000..70c1b6e20 --- /dev/null +++ b/repos/effect/.changeset/pre/http-client-redirect-preprocessing-recovery.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpClient.followRedirects` bypassing response-level recovery when request preprocessing fails. diff --git a/repos/effect/.changeset/pre/http-client-response-schema-json-options.md b/repos/effect/.changeset/pre/http-client-response-schema-json-options.md new file mode 100644 index 000000000..b4a867afa --- /dev/null +++ b/repos/effect/.changeset/pre/http-client-response-schema-json-options.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Fix `HttpClientResponse.schemaJson` and `HttpClientResponse.schemaNoBody` to apply parse options when decoding response +schemas. diff --git a/repos/effect/.changeset/pre/http-head-stream-scope.md b/repos/effect/.changeset/pre/http-head-stream-scope.md new file mode 100644 index 000000000..93f35c109 --- /dev/null +++ b/repos/effect/.changeset/pre/http-head-stream-scope.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Close request scopes for streaming HEAD responses. diff --git a/repos/effect/.changeset/pre/http-query-method.md b/repos/effect/.changeset/pre/http-query-method.md new file mode 100644 index 000000000..debdb31f3 --- /dev/null +++ b/repos/effect/.changeset/pre/http-query-method.md @@ -0,0 +1,8 @@ +--- +"effect": patch +"@effect/openapi-generator": patch +--- + +Add HTTP `QUERY` support to clients, routers, HttpApi endpoints, and AI request metadata. Configure CORS support through `allowedMethods`; defaults are unchanged. + +OpenAPI 3.1 output represents `QUERY` through `x-oai-additionalOperations`, which requires consumer support for that extension. The OpenAPI generator accepts the extension and the native OpenAPI 3.2 `query` field. diff --git a/repos/effect/.changeset/pre/http-response-content-length.md b/repos/effect/.changeset/pre/http-response-content-length.md new file mode 100644 index 000000000..55326a913 --- /dev/null +++ b/repos/effect/.changeset/pre/http-response-content-length.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve `Content-Length` headers in `HttpServerResponse.fromWeb`. diff --git a/repos/effect/.changeset/pre/http-router-normalized-prefix.md b/repos/effect/.changeset/pre/http-router-normalized-prefix.md new file mode 100644 index 000000000..a19f44155 --- /dev/null +++ b/repos/effect/.changeset/pre/http-router-normalized-prefix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize router prefixes before removing them from handler request URLs. diff --git a/repos/effect/.changeset/pre/http-runner-path-boundary.md b/repos/effect/.changeset/pre/http-runner-path-boundary.md new file mode 100644 index 000000000..412f194b1 --- /dev/null +++ b/repos/effect/.changeset/pre/http-runner-path-boundary.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `HttpRunner` HTTP and WebSocket client URLs adding an extra leading slash to slash-prefixed paths. Insert the address/path separator only when it is missing, preserving intentional leading and interior slashes. + +This path correction is normally masked by router normalization, but prevents route misses for non-root paths when duplicate-slash normalization is disabled. Applications that compensate for the extra slash may need to remove that compensation. Router defaults and shared trailing-slash handling are unchanged. diff --git a/repos/effect/.changeset/pre/http-web-handler-cold-start.md b/repos/effect/.changeset/pre/http-web-handler-cold-start.md new file mode 100644 index 000000000..5fb4aabd0 --- /dev/null +++ b/repos/effect/.changeset/pre/http-web-handler-cold-start.md @@ -0,0 +1,8 @@ +--- +"effect": patch +--- + +Reduce cold start cost of `HttpRouter` and `HttpEffect` web handlers. + +- `HttpServerRespondable` no longer imports `Schema` to detect schema errors, which removes the Schema modules from bundles that do not otherwise use them (about 23% of a minimal `HttpRouter` bundle). +- `HttpRouter.toWebHandler`, `HttpEffect.toWebHandlerLayer` and `HttpEffect.toWebHandlerLayerWith` now build the layer immediately instead of on the first request. A failed build never surfaces as an unhandled rejection; every request rejects with the build error instead. diff --git a/repos/effect/.changeset/pre/httpapi-client-literal-actions.md b/repos/effect/.changeset/pre/httpapi-client-literal-actions.md new file mode 100644 index 000000000..5fb5f6a85 --- /dev/null +++ b/repos/effect/.changeset/pre/httpapi-client-literal-actions.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Preserve literal suffixes such as `:wait` in `/operations/:id:wait` across `HttpApiClient`, `HttpApiBuilder`, and OpenAPI paths. + +Server routes without a params schema keep their existing matching for `RouteContext` consumers. Schemas whose keys cannot be enumerated keep the existing fallback. diff --git a/repos/effect/.changeset/pre/httpapi-middleware-error-dedupe.md b/repos/effect/.changeset/pre/httpapi-middleware-error-dedupe.md new file mode 100644 index 000000000..76d74654b --- /dev/null +++ b/repos/effect/.changeset/pre/httpapi-middleware-error-dedupe.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiMiddleware`-declared errors being duplicated and mis-encoded. diff --git a/repos/effect/.changeset/pre/httpapi-sse-decode-options.md b/repos/effect/.changeset/pre/httpapi-sse-decode-options.md new file mode 100644 index 000000000..eb84e9032 --- /dev/null +++ b/repos/effect/.changeset/pre/httpapi-sse-decode-options.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow generated `HttpApiClient` methods and `AtomHttpApi` queries and mutations to accept native SSE decode options per call through the request's `sseOptions` field. diff --git a/repos/effect/.changeset/pre/improve-json-schema-import.md b/repos/effect/.changeset/pre/improve-json-schema-import.md new file mode 100644 index 000000000..27024c853 --- /dev/null +++ b/repos/effect/.changeset/pre/improve-json-schema-import.md @@ -0,0 +1,57 @@ +--- +"effect": patch +--- + +Improve `SchemaRepresentation.fromJsonSchemaDocument` and `fromJsonSchemaMultiDocument`: + +- Import `{ not: {} }` as `Schema.Never` (#8137). + +- Import closed records with one `patternProperties` entry, `additionalProperties: false`, and no declared or required properties when `patterns: "apply"` is enabled. These were previously rejected. + + ```json + { + "type": "object", + "patternProperties": { "^a": { "type": "number" } }, + "additionalProperties": false + } + ``` + + ```ts + Schema.Record( + Schema.String.check(Schema.isPattern(/^a/)), + Schema.Finite + ) + ``` + +- Reject open patterned objects with `patterns: "apply"` instead of generating incompatible TypeScript index signatures. + + ```json + { + "type": "object", + "patternProperties": { "^a": { "type": "number" } }, + "additionalProperties": true + } + ``` + + Import now explains that the generated TypeScript index signatures would give incorrect types to unmatched keys, and reports the source path. The same applies when `additionalProperties` is omitted or `{}`. Patterns can still be combined with a closed object in `allOf` when the result has a finite set of keys. Use `patterns: "ignore"` only if you intend to discard the pattern and its value constraints. + +- Reject references inside a subschema with its own `$id` instead of potentially resolving against the wrong definitions. Resolve or flatten these references before importing. A `$id` on the document root remains supported. + + ```json + { + "$id": "https://example.com/root", + "$defs": { "Value": { "type": "string" } }, + "type": "object", + "properties": { + "child": { + "$id": "child", + "$defs": { "Value": { "type": "number" } }, + "$ref": "#/$defs/Value" + } + } + } + ``` + + Here `child` refers to the nested numeric `Value`, not the root string `Value`. Import now reports that references inside a subschema with its own `$id` are unsupported instead of incorrectly using the root definition. + +- Explain import failures using JSON Schema keyword names, the reason for rejection, and the source path. Reference errors distinguish missing definitions, unsupported reference formats, and circular aliases. Pattern errors explain how to opt in for trusted schemas or explicitly discard the constraints. diff --git a/repos/effect/.changeset/pre/indexeddb-out-of-line-primary-keys.md b/repos/effect/.changeset/pre/indexeddb-out-of-line-primary-keys.md new file mode 100644 index 000000000..30b66a4e5 --- /dev/null +++ b/repos/effect/.changeset/pre/indexeddb-out-of-line-primary-keys.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Preserve out-of-line primary keys in IndexedDB query results, including secondary-index reads. diff --git a/repos/effect/.changeset/pre/indexeddb-stream-query-limits.md b/repos/effect/.changeset/pre/indexeddb-stream-query-limits.md new file mode 100644 index 000000000..c12593165 --- /dev/null +++ b/repos/effect/.changeset/pre/indexeddb-stream-query-limits.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Keep streamed IndexedDB selects within their query limits. diff --git a/repos/effect/.changeset/pre/isolate-unencodable-hydration.md b/repos/effect/.changeset/pre/isolate-unencodable-hydration.md new file mode 100644 index 000000000..fa72c498e --- /dev/null +++ b/repos/effect/.changeset/pre/isolate-unencodable-hydration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent unencodable atom values from aborting dehydration of the rest of an atom registry. diff --git a/repos/effect/.changeset/large-postgres-messages.md b/repos/effect/.changeset/pre/large-postgres-messages.md similarity index 100% rename from repos/effect/.changeset/large-postgres-messages.md rename to repos/effect/.changeset/pre/large-postgres-messages.md diff --git a/repos/effect/.changeset/pre/latest-dependencies.md b/repos/effect/.changeset/pre/latest-dependencies.md new file mode 100644 index 000000000..d6459ac3b --- /dev/null +++ b/repos/effect/.changeset/pre/latest-dependencies.md @@ -0,0 +1,8 @@ +--- +"@effect/doctest": patch +"@effect/platform-deno": patch +"@effect/sql-d1": patch +"@effect/sql-mysql2": patch +--- + +Update Rolldown, the Deno Redis driver, Cloudflare Workers types, and MySQL dependencies to their latest releases. diff --git a/repos/effect/.changeset/pre/layer-error-observer-types.md b/repos/effect/.changeset/pre/layer-error-observer-types.md new file mode 100644 index 000000000..24bcdb3e2 --- /dev/null +++ b/repos/effect/.changeset/pre/layer-error-observer-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Layer.tapError` and `Layer.tapCause` to require observers that accept the source layer's complete error type. diff --git a/repos/effect/.changeset/pre/layer-span-trace-options.md b/repos/effect/.changeset/pre/layer-span-trace-options.md new file mode 100644 index 000000000..d0cb11a3b --- /dev/null +++ b/repos/effect/.changeset/pre/layer-span-trace-options.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor `captureStackTrace` in both forms of `Layer.withSpan`. Layer construction diagnostics previously reported a location inside `Layer.ts` instead of the `withSpan` call site, and ignored `captureStackTrace: false` or a supplied lazy stack. diff --git a/repos/effect/.changeset/pre/layermap-preloaded-acquisition-errors.md b/repos/effect/.changeset/pre/layermap-preloaded-acquisition-errors.md new file mode 100644 index 000000000..402864fb9 --- /dev/null +++ b/repos/effect/.changeset/pre/layermap-preloaded-acquisition-errors.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Preserve resource acquisition errors on `LayerMap.Service` when `preload: true` is set. The yielded service instance and its `get`, `contextEffect`, and `contextEffectOption` accessors now retain the resource error type because a resource can fail when reacquired, even if preloading succeeded. + +Consumers that assumed these accessors had a `never` error must handle the resource error. Runtime behavior is unchanged. diff --git a/repos/effect/.changeset/pre/lazy-bun-redis-import.md b/repos/effect/.changeset/pre/lazy-bun-redis-import.md new file mode 100644 index 000000000..9255d6d97 --- /dev/null +++ b/repos/effect/.changeset/pre/lazy-bun-redis-import.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Load Bun's `RedisClient` lazily in `BunRedis`. diff --git a/repos/effect/.changeset/pre/lazy-undici-loading.md b/repos/effect/.changeset/pre/lazy-undici-loading.md new file mode 100644 index 000000000..9c767e4ea --- /dev/null +++ b/repos/effect/.changeset/pre/lazy-undici-loading.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Defer loading Undici until an Undici-backed layer is acquired, preventing Node HTTP client imports from replacing Node's global fetch dispatcher. Import Undici APIs from `@effect/platform-node/Undici` instead of the package root. diff --git a/repos/effect/.changeset/pre/libsql-transaction-client-isolation.md b/repos/effect/.changeset/pre/libsql-transaction-client-isolation.md new file mode 100644 index 000000000..186ff4348 --- /dev/null +++ b/repos/effect/.changeset/pre/libsql-transaction-client-isolation.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-libsql": patch +--- + +Isolate transaction contexts between separately created libSQL clients. diff --git a/repos/effect/.changeset/pre/logger-complete-file-writes.md b/repos/effect/.changeset/pre/logger-complete-file-writes.md new file mode 100644 index 000000000..0c2c6983d --- /dev/null +++ b/repos/effect/.changeset/pre/logger-complete-file-writes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Logger.toFile` dropping the remainder of a log batch when a successful file write writes only part of the buffer. File logging now uses the complete-write contract; write errors continue to be ignored. diff --git a/repos/effect/.changeset/pre/mcp-http-resource-template-origins.md b/repos/effect/.changeset/pre/mcp-http-resource-template-origins.md new file mode 100644 index 000000000..b94ed9b0d --- /dev/null +++ b/repos/effect/.changeset/pre/mcp-http-resource-template-origins.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `McpServer` HTTP resource templates failing to resolve. diff --git a/repos/effect/.changeset/pre/mcp-identified-output-schema.md b/repos/effect/.changeset/pre/mcp-identified-output-schema.md new file mode 100644 index 000000000..503ea7349 --- /dev/null +++ b/repos/effect/.changeset/pre/mcp-identified-output-schema.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Normalize identified MCP output schemas to object roots across protocol versions. +Support identified object schemas in MCP elicitation requests. diff --git a/repos/effect/.changeset/pre/mcp-prompt-decoded-parameter-types.md b/repos/effect/.changeset/pre/mcp-prompt-decoded-parameter-types.md new file mode 100644 index 000000000..180a6f1ad --- /dev/null +++ b/repos/effect/.changeset/pre/mcp-prompt-decoded-parameter-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `McpServer.registerPrompt` callback types to use decoded prompt parameters. diff --git a/repos/effect/.changeset/pre/mcp-prompt-titles.md b/repos/effect/.changeset/pre/mcp-prompt-titles.md new file mode 100644 index 000000000..42b30b54e --- /dev/null +++ b/repos/effect/.changeset/pre/mcp-prompt-titles.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Support prompt titles in `McpServer.prompt` and `McpServer.registerPrompt`. diff --git a/repos/effect/.changeset/pre/mcp-server-instructions.md b/repos/effect/.changeset/pre/mcp-server-instructions.md new file mode 100644 index 000000000..1302b3381 --- /dev/null +++ b/repos/effect/.changeset/pre/mcp-server-instructions.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add an `instructions` option to MCP servers for initialization and discovery responses. diff --git a/repos/effect/.changeset/mcp-structured-content-object.md b/repos/effect/.changeset/pre/mcp-structured-content-object.md similarity index 100% rename from repos/effect/.changeset/mcp-structured-content-object.md rename to repos/effect/.changeset/pre/mcp-structured-content-object.md diff --git a/repos/effect/.changeset/pre/memory-message-storage-clear-controls.md b/repos/effect/.changeset/pre/memory-message-storage-clear-controls.md new file mode 100644 index 000000000..d4f524b69 --- /dev/null +++ b/repos/effect/.changeset/pre/memory-message-storage-clear-controls.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove queued control envelopes when clearing an address from in-memory message storage. diff --git a/repos/effect/.changeset/pre/metric-attribute-order-identity.md b/repos/effect/.changeset/pre/metric-attribute-order-identity.md new file mode 100644 index 000000000..608c6d396 --- /dev/null +++ b/repos/effect/.changeset/pre/metric-attribute-order-identity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure metrics with equal attributes share a series regardless of attribute insertion order. diff --git a/repos/effect/.changeset/pre/metric-registry-isolation.md b/repos/effect/.changeset/pre/metric-registry-isolation.md new file mode 100644 index 000000000..68aa520b5 --- /dev/null +++ b/repos/effect/.changeset/pre/metric-registry-isolation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix metrics reused across different `MetricRegistry` services to read and update the active registry while preserving each registry's values when revisited. diff --git a/repos/effect/.changeset/pre/model-field-option-undefined.md b/repos/effect/.changeset/pre/model-field-option-undefined.md new file mode 100644 index 000000000..3bca7a691 --- /dev/null +++ b/repos/effect/.changeset/pre/model-field-option-undefined.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Model.FieldOption` to preserve omitted variants. diff --git a/repos/effect/.changeset/pre/modern-mice-discover.md b/repos/effect/.changeset/pre/modern-mice-discover.md new file mode 100644 index 000000000..06985bb85 --- /dev/null +++ b/repos/effect/.changeset/pre/modern-mice-discover.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add server support for MCP protocol version 2026-07-28 through 2026_07_28 diff --git a/repos/effect/.changeset/pre/mssql-binary-parameters.md b/repos/effect/.changeset/pre/mssql-binary-parameters.md new file mode 100644 index 000000000..9ceac54a6 --- /dev/null +++ b/repos/effect/.changeset/pre/mssql-binary-parameters.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-mssql": patch +--- + +Fix automatic binary parameter binding in `@effect/sql-mssql`. diff --git a/repos/effect/.changeset/pre/mssql-ntlm-domain.md b/repos/effect/.changeset/pre/mssql-ntlm-domain.md new file mode 100644 index 000000000..eaa55815f --- /dev/null +++ b/repos/effect/.changeset/pre/mssql-ntlm-domain.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-mssql": patch +--- + +Forward the configured domain to the SQL Server driver so NTLM clients can be constructed with a supplied domain. diff --git a/repos/effect/.changeset/pre/multipart-binary-client-file-type.md b/repos/effect/.changeset/pre/multipart-binary-client-file-type.md new file mode 100644 index 000000000..3bd11c39e --- /dev/null +++ b/repos/effect/.changeset/pre/multipart-binary-client-file-type.md @@ -0,0 +1,7 @@ +--- +"@effect/openapi-generator": patch +--- + +Generated HTTP clients now type `multipart/form-data` fields with `format: binary` as `File | Blob` instead of `string`. `httpapi` output is unchanged. + +Referenced components containing binary fields use `*Multipart` exports, such as `UploadBodyMultipart`. If a component is used only by multipart requests, update imports from its original name to the generated name. Components also used by JSON requests or responses keep their original exports and string fields. diff --git a/repos/effect/.changeset/pre/multipart-streamed-part-guard.md b/repos/effect/.changeset/pre/multipart-streamed-part-guard.md new file mode 100644 index 000000000..f593fc224 --- /dev/null +++ b/repos/effect/.changeset/pre/multipart-streamed-part-guard.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Multipart.isStreamPart` to recognize only a text `Field` or streamed `File`, while preserving `Multipart.isPart` for all branded multipart parts, including `PersistedFile` values. diff --git a/repos/effect/.changeset/mutable-arrays-preserve.md b/repos/effect/.changeset/pre/mutable-arrays-preserve.md similarity index 100% rename from repos/effect/.changeset/mutable-arrays-preserve.md rename to repos/effect/.changeset/pre/mutable-arrays-preserve.md diff --git a/repos/effect/.changeset/pre/mutable-list-bulk-prepend-tail.md b/repos/effect/.changeset/pre/mutable-list-bulk-prepend-tail.md new file mode 100644 index 000000000..61e8ed55a --- /dev/null +++ b/repos/effect/.changeset/pre/mutable-list-bulk-prepend-tail.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve values added to an empty `MutableList` by `prependAll` when appending more values. diff --git a/repos/effect/.changeset/pre/name-random-services.md b/repos/effect/.changeset/pre/name-random-services.md new file mode 100644 index 000000000..2fef9ba2e --- /dev/null +++ b/repos/effect/.changeset/pre/name-random-services.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Export the `Random.Random` service interface and `Metric.MetricRegistry` type so custom service implementations can be annotated without accessing `Context.Reference` phantom types. diff --git a/repos/effect/.changeset/pre/net-address-host-helpers.md b/repos/effect/.changeset/pre/net-address-host-helpers.md new file mode 100644 index 000000000..ea7d6ccd1 --- /dev/null +++ b/repos/effect/.changeset/pre/net-address-host-helpers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `NetAddress.formatHost` and `NetAddress.inetAddressFromHostString` for socket APIs that accept numeric hosts and ports separately, preserving IPv6 scope IDs. Add `NetAddress.scopeIdsFromInterfaces` to build a scope map for resolving named IPv6 zones from supplied network interface entries without performing operating-system lookups. diff --git a/repos/effect/.changeset/pre/net-address-values.md b/repos/effect/.changeset/pre/net-address-values.md new file mode 100644 index 000000000..b6db60e64 --- /dev/null +++ b/repos/effect/.changeset/pre/net-address-values.md @@ -0,0 +1,14 @@ +--- +"effect": patch +"@effect/sql-pg": patch +"@effect/platform-node": patch +"@effect/platform-node-shared": patch +"@effect/platform-deno": patch +"@effect/platform-bun": patch +--- + +Add `NetAddress` under `effect/unstable/net` for MAC, IP, internet socket, and Unix socket addresses, with checked parsing, schemas, equality, canonical string serialization, and URL formatting. Companion modules `IpInterface` and `IpNetwork` represent IP interfaces and CIDR networks. + +HTTP and socket servers now expose `NetAddress.SocketAddress`. Replace TCP `hostname` access with `NetAddress.formatIp(address.address)` and use `UnixPathAddress.path` for Unix sockets. URL helpers bracket IPv6 addresses and reject scoped IPv6. Bun and Deno HTTP server layers can now fail with `ServeError` when listener address conversion fails. + +PostgreSQL `inet` values now use `IpInterface`; `cidr` values use `IpNetwork` and reject addresses with host bits set. diff --git a/repos/effect/.changeset/ninety-books-sit.md b/repos/effect/.changeset/pre/ninety-books-sit.md similarity index 100% rename from repos/effect/.changeset/ninety-books-sit.md rename to repos/effect/.changeset/pre/ninety-books-sit.md diff --git a/repos/effect/.changeset/pre/node-http-status-text.md b/repos/effect/.changeset/pre/node-http-status-text.md new file mode 100644 index 000000000..b83133d66 --- /dev/null +++ b/repos/effect/.changeset/pre/node-http-status-text.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Forward custom and empty status text from NodeHttpServer responses. diff --git a/repos/effect/.changeset/pre/node-response-preserve-bytes.md b/repos/effect/.changeset/pre/node-response-preserve-bytes.md new file mode 100644 index 000000000..add6daba8 --- /dev/null +++ b/repos/effect/.changeset/pre/node-response-preserve-bytes.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Preserve response bytes when selecting the text reader in Node HTTP clients. diff --git a/repos/effect/.changeset/pre/node-sink-cancel-drain.md b/repos/effect/.changeset/pre/node-sink-cancel-drain.md new file mode 100644 index 000000000..257aead2e --- /dev/null +++ b/repos/effect/.changeset/pre/node-sink-cancel-drain.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Prevent Node sinks from submitting buffered writes after interruption while waiting for writable backpressure. diff --git a/repos/effect/.changeset/pre/node-sink-finalization-errors.md b/repos/effect/.changeset/pre/node-sink-finalization-errors.md new file mode 100644 index 000000000..308442c81 --- /dev/null +++ b/repos/effect/.changeset/pre/node-sink-finalization-errors.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Fix `NodeSink` hanging when a writable reports an error during finalization. diff --git a/repos/effect/.changeset/pre/node-stream-buffer-size.md b/repos/effect/.changeset/pre/node-stream-buffer-size.md new file mode 100644 index 000000000..82b55372d --- /dev/null +++ b/repos/effect/.changeset/pre/node-stream-buffer-size.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Remove the ignored `bufferSize` option from `NodeStream`. diff --git a/repos/effect/.changeset/pre/node-watch-relative.md b/repos/effect/.changeset/pre/node-watch-relative.md new file mode 100644 index 000000000..792683eba --- /dev/null +++ b/repos/effect/.changeset/pre/node-watch-relative.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Fix watch event classification for targets outside the current working directory. diff --git a/repos/effect/.changeset/pre/node-worker-unsafe-send-envelope.md b/repos/effect/.changeset/pre/node-worker-unsafe-send-envelope.md new file mode 100644 index 000000000..999baa91a --- /dev/null +++ b/repos/effect/.changeset/pre/node-worker-unsafe-send-envelope.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Preserve reply payloads sent through `NodeWorkerRunner.sendUnsafe`. diff --git a/repos/effect/.changeset/pre/node-writeall-empty.md b/repos/effect/.changeset/pre/node-writeall-empty.md new file mode 100644 index 000000000..f503e0d70 --- /dev/null +++ b/repos/effect/.changeset/pre/node-writeall-empty.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Allow `File.writeAll` to accept empty buffers. diff --git a/repos/effect/.changeset/normalize-collection-counts.md b/repos/effect/.changeset/pre/normalize-collection-counts.md similarity index 100% rename from repos/effect/.changeset/normalize-collection-counts.md rename to repos/effect/.changeset/pre/normalize-collection-counts.md diff --git a/repos/effect/.changeset/pre/number-remainder-negative-zero-dividend.md b/repos/effect/.changeset/pre/number-remainder-negative-zero-dividend.md new file mode 100644 index 000000000..054499696 --- /dev/null +++ b/repos/effect/.changeset/pre/number-remainder-negative-zero-dividend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Number.remainder` to preserve negative-zero dividends with ordinary finite divisors. diff --git a/repos/effect/.changeset/olive-queues-persist.md b/repos/effect/.changeset/pre/olive-queues-persist.md similarity index 100% rename from repos/effect/.changeset/olive-queues-persist.md rename to repos/effect/.changeset/pre/olive-queues-persist.md diff --git a/repos/effect/.changeset/pre/openai-image-strings.md b/repos/effect/.changeset/pre/openai-image-strings.md new file mode 100644 index 000000000..5a8aae24e --- /dev/null +++ b/repos/effect/.changeset/pre/openai-image-strings.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +--- + +Preserve image data supplied as strings in OpenAI Responses and OpenAI-compatible chat requests. diff --git a/repos/effect/.changeset/pre/openai-optional-sequence-number.md b/repos/effect/.changeset/pre/openai-optional-sequence-number.md new file mode 100644 index 000000000..ffd414cd8 --- /dev/null +++ b/repos/effect/.changeset/pre/openai-optional-sequence-number.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Allow OpenAI-compatible Responses API stream events to omit `sequence_number`. diff --git a/repos/effect/.changeset/pre/openai-web-search-api-sources.md b/repos/effect/.changeset/pre/openai-web-search-api-sources.md new file mode 100644 index 000000000..62c9e6524 --- /dev/null +++ b/repos/effect/.changeset/pre/openai-web-search-api-sources.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Support API sources in OpenAI web search. Narrow sources by `type` before reading `url` or `name`. diff --git a/repos/effect/.changeset/pre/openai-web-search-outcomes.md b/repos/effect/.changeset/pre/openai-web-search-outcomes.md new file mode 100644 index 000000000..e6129dcf2 --- /dev/null +++ b/repos/effect/.changeset/pre/openai-web-search-outcomes.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Report failed OpenAI provider-executed tool calls as failure tool results in both `generateText` and `streamText`, preserving the call details. diff --git a/repos/effect/.changeset/openapi-omitted-additional-properties.md b/repos/effect/.changeset/pre/openapi-omitted-additional-properties.md similarity index 100% rename from repos/effect/.changeset/openapi-omitted-additional-properties.md rename to repos/effect/.changeset/pre/openapi-omitted-additional-properties.md diff --git a/repos/effect/.changeset/pre/openrouter-alpha-decisions.md b/repos/effect/.changeset/pre/openrouter-alpha-decisions.md new file mode 100644 index 000000000..8dd39d844 --- /dev/null +++ b/repos/effect/.changeset/pre/openrouter-alpha-decisions.md @@ -0,0 +1,7 @@ +--- +"@effect/ai-openrouter": patch +--- + +Add `OpenRouterDecisionModel` for OpenRouter's alpha Decisions API. + +Custom `OpenRouterClient.Service` implementations and mocks must now provide `createDecisions`. diff --git a/repos/effect/.changeset/pre/openrouter-disjoint-usage-details.md b/repos/effect/.changeset/pre/openrouter-disjoint-usage-details.md new file mode 100644 index 000000000..8b36a80db --- /dev/null +++ b/repos/effect/.changeset/pre/openrouter-disjoint-usage-details.md @@ -0,0 +1,7 @@ +--- +"@effect/ai-openrouter": patch +--- + +Fix negative text and uncached token usage when reasoning or cached counts exceed their parent totals. Treat these counts as disjoint, adding them to the total and retaining the parent count as text or uncached usage. + +Disjoint counts at or below their parent totals remain indistinguishable from subsets, so their totals are still undercounted. diff --git a/repos/effect/.changeset/pre/openrouter-encrypted-tool-finish.md b/repos/effect/.changeset/pre/openrouter-encrypted-tool-finish.md new file mode 100644 index 000000000..4f5006ac1 --- /dev/null +++ b/repos/effect/.changeset/pre/openrouter-encrypted-tool-finish.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Report `tool-calls` instead of `unknown` when an OpenRouter stream ends with `stop` after emitting tool calls and nonempty encrypted reasoning. diff --git a/repos/effect/.changeset/pre/openrouter-strict-json-schema.md b/repos/effect/.changeset/pre/openrouter-strict-json-schema.md new file mode 100644 index 000000000..736612759 --- /dev/null +++ b/repos/effect/.changeset/pre/openrouter-strict-json-schema.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Exclude the provider-only `strictJsonSchema` option from outgoing OpenRouter request bodies while preserving structured-output and tool strictness settings. diff --git a/repos/effect/.changeset/pre/opentelemetry-delta-interval-starts.md b/repos/effect/.changeset/pre/opentelemetry-delta-interval-starts.md new file mode 100644 index 000000000..c536f6823 --- /dev/null +++ b/repos/effect/.changeset/pre/opentelemetry-delta-interval-starts.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Use collection interval starts for OpenTelemetry delta metrics. diff --git a/repos/effect/.changeset/pre/optic-projection-replacement.md b/repos/effect/.changeset/pre/optic-projection-replacement.md new file mode 100644 index 000000000..3523631ae --- /dev/null +++ b/repos/effect/.changeset/pre/optic-projection-replacement.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `Optic.pick` and `Optic.omit` delete focused optional fields omitted from a replacement. diff --git a/repos/effect/.changeset/pre/optic-string-index-delete.md b/repos/effect/.changeset/pre/optic-string-index-delete.md new file mode 100644 index 000000000..8468eda4a --- /dev/null +++ b/repos/effect/.changeset/pre/optic-string-index-delete.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Optic.optionalKey` to splice tuple elements selected by string indices. diff --git a/repos/effect/.changeset/pre/optional-never-arbitrary.md b/repos/effect/.changeset/pre/optional-never-arbitrary.md new file mode 100644 index 000000000..7c6a3cd62 --- /dev/null +++ b/repos/effect/.changeset/pre/optional-never-arbitrary.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Treat `Never` as an uninhabited branch during `Arbitrary.schema` derivation. Schemas with another finite generation path, including optional properties, unions, and empty collections, no longer fail derivation. diff --git a/repos/effect/.changeset/pre/order-consumed-criteria.md b/repos/effect/.changeset/pre/order-consumed-criteria.md new file mode 100644 index 000000000..85dd26e64 --- /dev/null +++ b/repos/effect/.changeset/pre/order-consumed-criteria.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Order.combineAll` consuming one-shot iterables after the first comparison. diff --git a/repos/effect/.changeset/pre/otlp-disabled-batch-flush.md b/repos/effect/.changeset/pre/otlp-disabled-batch-flush.md new file mode 100644 index 000000000..9ab34b05a --- /dev/null +++ b/repos/effect/.changeset/pre/otlp-disabled-batch-flush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix disabled OTLP batching to skip empty exports and avoid resending buffered items. diff --git a/repos/effect/.changeset/pre/otlp-tracer-span-performance.md b/repos/effect/.changeset/pre/otlp-tracer-span-performance.md new file mode 100644 index 000000000..2319594b7 --- /dev/null +++ b/repos/effect/.changeset/pre/otlp-tracer-span-performance.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Speed up `OtlpTracer` span creation and export. Spans now allocate identifiers, attributes, and events lazily, and `Encoding.randomHex` produces flat strings for 16 and 32 character identifiers so serialization no longer flattens ropes. diff --git a/repos/effect/.changeset/pre/parameterize-persistence-lookup-keys.md b/repos/effect/.changeset/pre/parameterize-persistence-lookup-keys.md new file mode 100644 index 000000000..92960c36a --- /dev/null +++ b/repos/effect/.changeset/pre/parameterize-persistence-lookup-keys.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Parameterize persistence lookup keys in both SQL backing stores' `getMany` queries. diff --git a/repos/effect/.changeset/pre/partitioned-semaphore-stale-cleanup.md b/repos/effect/.changeset/pre/partitioned-semaphore-stale-cleanup.md new file mode 100644 index 000000000..72b3fba03 --- /dev/null +++ b/repos/effect/.changeset/pre/partitioned-semaphore-stale-cleanup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `PartitionedSemaphore` leaving a new waiter suspended when a previously resumed waiter for the same partition is interrupted before its acquisition completes. diff --git a/repos/effect/.changeset/pre/persisted-cache-lookup-throw.md b/repos/effect/.changeset/pre/persisted-cache-lookup-throw.md new file mode 100644 index 000000000..81b54be07 --- /dev/null +++ b/repos/effect/.changeset/pre/persisted-cache-lookup-throw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Persist synchronous defects thrown by `PersistedCache` lookups. diff --git a/repos/effect/.changeset/pre/pg-explicit-ssl-precedence.md b/repos/effect/.changeset/pre/pg-explicit-ssl-precedence.md new file mode 100644 index 000000000..e52b6246f --- /dev/null +++ b/repos/effect/.changeset/pre/pg-explicit-ssl-precedence.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Honor explicit `ssl` options when PostgreSQL URLs use `sslmode=prefer` or `sslmode=allow`. diff --git a/repos/effect/.changeset/pre/pg-listen-connection-errors.md b/repos/effect/.changeset/pre/pg-listen-connection-errors.md new file mode 100644 index 000000000..fe0e5cd61 --- /dev/null +++ b/repos/effect/.changeset/pre/pg-listen-connection-errors.md @@ -0,0 +1,7 @@ +--- +"@effect/sql-pg": patch +--- + +Preserve the original `SqlError` when a PostgreSQL LISTEN connection fails after registration, allowing `Stream.retry` to acquire a new listener. Intentional scope closure still interrupts consumers. + +Notification queues returned by `PgClient.listen` and `PgConnection.listen` now carry `SqlError`. Update explicit queue and stream type annotations to include this error. diff --git a/repos/effect/.changeset/pre/pg-regclass-codec.md b/repos/effect/.changeset/pre/pg-regclass-codec.md new file mode 100644 index 000000000..f2a4331d9 --- /dev/null +++ b/repos/effect/.changeset/pre/pg-regclass-codec.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Decode binary `regclass` and `regclass[]` values as unsigned numeric OIDs, fixing the migration runner's table-existence check. Cast `regclass` values to `text` in SQL to return relation names. diff --git a/repos/effect/.changeset/pre/pg-sslmode-compatibility.md b/repos/effect/.changeset/pre/pg-sslmode-compatibility.md new file mode 100644 index 000000000..9b7cf2eef --- /dev/null +++ b/repos/effect/.changeset/pre/pg-sslmode-compatibility.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Support `sslmode=prefer` and `sslmode=allow` by trying TLS first, then plaintext if the server declines `SSLRequest`. Unlike libpq, `allow` also tries TLS first. Explicit `ssl` overrides and certificate verification are unchanged. TLS handshake and certificate failures remain fatal. Cancellation never downgrades a TLS session to plaintext. diff --git a/repos/effect/.changeset/pre/pg-startup-parameters.md b/repos/effect/.changeset/pre/pg-startup-parameters.md new file mode 100644 index 000000000..876df5954 --- /dev/null +++ b/repos/effect/.changeset/pre/pg-startup-parameters.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Add structured `startupParameters` and opaque `startupOptions` to connection config, and restore support for URL `options`. Explicit `startupOptions` overrides URL `options`; the startup packet field remains `options`. Send session defaults in every physical connection's startup packet, including pooled replacements, with config validation and documented application-name precedence. diff --git a/repos/effect/.changeset/pre/pglite-json-string-values.md b/repos/effect/.changeset/pre/pglite-json-string-values.md new file mode 100644 index 000000000..f30fa9521 --- /dev/null +++ b/repos/effect/.changeset/pre/pglite-json-string-values.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pglite": patch +--- + +Preserve string values passed to `sql.json` when using PGlite. diff --git a/repos/effect/.changeset/pre/pipeline-root-stdin.md b/repos/effect/.changeset/pre/pipeline-root-stdin.md new file mode 100644 index 000000000..1c380f884 --- /dev/null +++ b/repos/effect/.changeset/pre/pipeline-root-stdin.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Route pipeline handle input through the first process. diff --git a/repos/effect/.changeset/pre/plain-tool-results.md b/repos/effect/.changeset/pre/plain-tool-results.md new file mode 100644 index 000000000..b58b40275 --- /dev/null +++ b/repos/effect/.changeset/pre/plain-tool-results.md @@ -0,0 +1,7 @@ +--- +"@effect/ai-openai": patch +"@effect/ai-anthropic": patch +"@effect/ai-openrouter": patch +--- + +Preserve plain-text client tool results when encoding provider requests. diff --git a/repos/effect/.changeset/pre/pool-preserve-reservations.md b/repos/effect/.changeset/pre/pool-preserve-reservations.md new file mode 100644 index 000000000..0f87426b2 --- /dev/null +++ b/repos/effect/.changeset/pre/pool-preserve-reservations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep `Pool.reserve` items out of shared circulation when other borrowers return or overlapping reservations close. Restore available slots only after the last reservation closes. diff --git a/repos/effect/.changeset/pre/port-http-api-builder-handler.md b/repos/effect/.changeset/pre/port-http-api-builder-handler.md new file mode 100644 index 000000000..a499e2fe2 --- /dev/null +++ b/repos/effect/.changeset/pre/port-http-api-builder-handler.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Port `HttpApiBuilder.handler` from v3 to define reusable endpoint callbacks with inferred request, response, error, and service types. diff --git a/repos/effect/.changeset/postgres-channel-names.md b/repos/effect/.changeset/pre/postgres-channel-names.md similarity index 100% rename from repos/effect/.changeset/postgres-channel-names.md rename to repos/effect/.changeset/pre/postgres-channel-names.md diff --git a/repos/effect/.changeset/pre/preserve-default-variant-class.md b/repos/effect/.changeset/pre/preserve-default-variant-class.md new file mode 100644 index 000000000..d263ec6ae --- /dev/null +++ b/repos/effect/.changeset/pre/preserve-default-variant-class.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve schema classes when extracting their default `VariantSchema` variant. diff --git a/repos/effect/.changeset/pre/preserve-negative-counter-deltas.md b/repos/effect/.changeset/pre/preserve-negative-counter-deltas.md new file mode 100644 index 000000000..d8783c189 --- /dev/null +++ b/repos/effect/.changeset/pre/preserve-negative-counter-deltas.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/opentelemetry": patch +--- + +Preserve negative counter deltas in OTLP and OpenTelemetry metric exports. diff --git a/repos/effect/.changeset/principled-collection-counts.md b/repos/effect/.changeset/pre/principled-collection-counts.md similarity index 100% rename from repos/effect/.changeset/principled-collection-counts.md rename to repos/effect/.changeset/pre/principled-collection-counts.md diff --git a/repos/effect/.changeset/pre/prompt-buffer-render.md b/repos/effect/.changeset/pre/prompt-buffer-render.md new file mode 100644 index 000000000..6cc05a921 --- /dev/null +++ b/repos/effect/.changeset/pre/prompt-buffer-render.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep the previous prompt frame visible until the next frame or submission is ready to display. diff --git a/repos/effect/.changeset/pre/prompt-lossless-text-serialization.md b/repos/effect/.changeset/pre/prompt-lossless-text-serialization.md new file mode 100644 index 000000000..ead45aa74 --- /dev/null +++ b/repos/effect/.changeset/pre/prompt-lossless-text-serialization.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve text parts and provider options when serializing prompts. diff --git a/repos/effect/.changeset/pre/prompt-response-files.md b/repos/effect/.changeset/pre/prompt-response-files.md new file mode 100644 index 000000000..0f11a7d89 --- /dev/null +++ b/repos/effect/.changeset/pre/prompt-response-files.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve generated files when converting AI responses to prompts. diff --git a/repos/effect/.changeset/pre/pubsub-sliding-single-subscriber.md b/repos/effect/.changeset/pre/pubsub-sliding-single-subscriber.md new file mode 100644 index 000000000..3584a0509 --- /dev/null +++ b/repos/effect/.changeset/pre/pubsub-sliding-single-subscriber.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix capacity-one PubSub subscriber cursors after sliding past messages, including duplicate delivery and invalid state when unsubscribing from a slid message. diff --git a/repos/effect/.changeset/pull-based-socket.md b/repos/effect/.changeset/pre/pull-based-socket.md similarity index 100% rename from repos/effect/.changeset/pull-based-socket.md rename to repos/effect/.changeset/pre/pull-based-socket.md diff --git a/repos/effect/.changeset/pre/queue-manual-flush.md b/repos/effect/.changeset/pre/queue-manual-flush.md new file mode 100644 index 000000000..c7f3fb423 --- /dev/null +++ b/repos/effect/.changeset/pre/queue-manual-flush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Queue.flush` and `Queue.flushUnsafe` for manually releasing pending takers, including after synchronous offers. diff --git a/repos/effect/.changeset/pre/queue-reentrant-producers.md b/repos/effect/.changeset/pre/queue-reentrant-producers.md new file mode 100644 index 000000000..3048f717e --- /dev/null +++ b/repos/effect/.changeset/pre/queue-reentrant-producers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Queue` message duplication, capacity overruns, and consumer defects when a resumed producer synchronously uses the same queue. Zero-capacity queues now reserve each handed-off message for its consumer before resuming the producer. diff --git a/repos/effect/.changeset/pre/queue-shutdown-unsafe.md b/repos/effect/.changeset/pre/queue-shutdown-unsafe.md new file mode 100644 index 000000000..f840a6966 --- /dev/null +++ b/repos/effect/.changeset/pre/queue-shutdown-unsafe.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Queue.shutdownUnsafe` to synchronously discard buffered messages and settle pending queue operations from callbacks. Both `Queue.shutdown` and `Queue.shutdownUnsafe` return `false` when the queue has already been shut down or completed; previously, `Queue.shutdown` always returned `true`. diff --git a/repos/effect/.changeset/quiet-buffers-decode.md b/repos/effect/.changeset/pre/quiet-buffers-decode.md similarity index 100% rename from repos/effect/.changeset/quiet-buffers-decode.md rename to repos/effect/.changeset/pre/quiet-buffers-decode.md diff --git a/repos/effect/.changeset/pre/quiet-buns-route.md b/repos/effect/.changeset/pre/quiet-buns-route.md new file mode 100644 index 000000000..e63500872 --- /dev/null +++ b/repos/effect/.changeset/pre/quiet-buns-route.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Preserve configured Bun routes when Effect HTTP handlers are installed or restored. diff --git a/repos/effect/.changeset/pre/quiet-graphs-simplify.md b/repos/effect/.changeset/pre/quiet-graphs-simplify.md new file mode 100644 index 000000000..cc52f4d7d --- /dev/null +++ b/repos/effect/.changeset/pre/quiet-graphs-simplify.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the redundant `Graph.Proto` interface. Use `Graph.Graph` when accepting any immutable graph. diff --git a/repos/effect/.changeset/pre/quiet-hosts-format.md b/repos/effect/.changeset/pre/quiet-hosts-format.md new file mode 100644 index 000000000..211c20263 --- /dev/null +++ b/repos/effect/.changeset/pre/quiet-hosts-format.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `NetAddress.formatNativeHost` and `NetAddress.formatMulticastInterface` with explicit platform and scope ID map parameters for native socket APIs. diff --git a/repos/effect/.changeset/pre/quiet-pandas-connect.md b/repos/effect/.changeset/pre/quiet-pandas-connect.md new file mode 100644 index 000000000..7fb8e2146 --- /dev/null +++ b/repos/effect/.changeset/pre/quiet-pandas-connect.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Support infallible Effect password providers with no service requirements, refreshed per PostgreSQL connection and schema dump. diff --git a/repos/effect/.changeset/quiet-pandas-listen.md b/repos/effect/.changeset/pre/quiet-pandas-listen.md similarity index 100% rename from repos/effect/.changeset/quiet-pandas-listen.md rename to repos/effect/.changeset/pre/quiet-pandas-listen.md diff --git a/repos/effect/.changeset/quiet-streams-take.md b/repos/effect/.changeset/pre/quiet-streams-take.md similarity index 100% rename from repos/effect/.changeset/quiet-streams-take.md rename to repos/effect/.changeset/pre/quiet-streams-take.md diff --git a/repos/effect/.changeset/pre/quiet-tables-resolve.md b/repos/effect/.changeset/pre/quiet-tables-resolve.md new file mode 100644 index 000000000..676b29e5e --- /dev/null +++ b/repos/effect/.changeset/pre/quiet-tables-resolve.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-do": patch +--- + +Prevent automatic scheduler yields inside storage-backed transactions to avoid Durable Object input-gate deadlocks when another fiber is queued. Explicit asynchronous operations inside transactions remain unsupported. diff --git a/repos/effect/.changeset/pre/random-exclusive-upper-endpoint.md b/repos/effect/.changeset/pre/random-exclusive-upper-endpoint.md new file mode 100644 index 000000000..ee55cc507 --- /dev/null +++ b/repos/effect/.changeset/pre/random-exclusive-upper-endpoint.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Random.nextBetween` and `Crypto.randomBetween` returning their exclusive upper bound when floating-point arithmetic rounds up. diff --git a/repos/effect/.changeset/pre/random-pg-prepared-names.md b/repos/effect/.changeset/pre/random-pg-prepared-names.md new file mode 100644 index 000000000..a7b7bf76d --- /dev/null +++ b/repos/effect/.changeset/pre/random-pg-prepared-names.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Namespace prepared statement names per connection to avoid collisions when backend sessions are shared. diff --git a/repos/effect/.changeset/pre/rate-limiter-reset-lifetime.md b/repos/effect/.changeset/pre/rate-limiter-reset-lifetime.md new file mode 100644 index 000000000..03547308d --- /dev/null +++ b/repos/effect/.changeset/pre/rate-limiter-reset-lifetime.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Report the exact remaining store lifetime in `RateLimiter` fixed-window `resetAfter` metadata when `onExceeded` is `"delay"`, instead of rounding up to a whole window. Admission, returned delays, and remaining-token counts are unchanged. diff --git a/repos/effect/.changeset/rcmap-invalidation-release.md b/repos/effect/.changeset/pre/rcmap-invalidation-release.md similarity index 100% rename from repos/effect/.changeset/rcmap-invalidation-release.md rename to repos/effect/.changeset/pre/rcmap-invalidation-release.md diff --git a/repos/effect/.changeset/pre/rcmap-throwing-lookup.md b/repos/effect/.changeset/pre/rcmap-throwing-lookup.md new file mode 100644 index 000000000..2e8027ee5 --- /dev/null +++ b/repos/effect/.changeset/pre/rcmap-throwing-lookup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RcMap` entries getting stuck when the lookup function throws synchronously. Later borrowers now receive the defect, and unused entries are released according to their idle TTL instead of permanently consuming capacity. diff --git a/repos/effect/.changeset/pre/rcref-acquisition-shutdown.md b/repos/effect/.changeset/pre/rcref-acquisition-shutdown.md new file mode 100644 index 000000000..1ee8dedc2 --- /dev/null +++ b/repos/effect/.changeset/pre/rcref-acquisition-shutdown.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep `RcRef` closed when an in-flight acquisition finishes after its owning scope has closed. Release the late-acquired resource and interrupt waiting borrowers instead of making the resource available again. diff --git a/repos/effect/.changeset/pre/rcref-release-ownership.md b/repos/effect/.changeset/pre/rcref-release-ownership.md new file mode 100644 index 000000000..53bd9947b --- /dev/null +++ b/repos/effect/.changeset/pre/rcref-release-ownership.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `RcRef` borrower cleanup from discarding replacement resources after invalidation or reopening a reference after its owner scope has closed. diff --git a/repos/effect/.changeset/pre/reason-annotate-context-only.md b/repos/effect/.changeset/pre/reason-annotate-context-only.md new file mode 100644 index 000000000..12b1b2c39 --- /dev/null +++ b/repos/effect/.changeset/pre/reason-annotate-context-only.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Type `Cause.Reason#annotate` as accepting a `Context` only. diff --git a/repos/effect/.changeset/pre/release-pg-savepoints.md b/repos/effect/.changeset/pre/release-pg-savepoints.md new file mode 100644 index 000000000..74e0f8312 --- /dev/null +++ b/repos/effect/.changeset/pre/release-pg-savepoints.md @@ -0,0 +1,15 @@ +--- +"effect": patch +"@effect/sql-pg": patch +"@effect/sql-pglite": patch +"@effect/sql-mysql2": patch +"@effect/sql-libsql": patch +"@effect/sql-sqlite-node": patch +"@effect/sql-sqlite-bun": patch +"@effect/sql-sqlite-react-native": patch +"@effect/sql-sqlite-wasm": patch +--- + +Release savepoints after nested transactions succeed or successfully roll back in PostgreSQL, PGlite, MySQL, libSQL, and the Node, Bun, React Native, and WASM SQLite clients. This frees PostgreSQL transaction locks before the outer transaction completes. + +Custom SQL clients can opt in through the new `releaseSavepoint` option. Clients that omit it are unchanged. diff --git a/repos/effect/.changeset/remove-msgpack.md b/repos/effect/.changeset/pre/remove-msgpack.md similarity index 100% rename from repos/effect/.changeset/remove-msgpack.md rename to repos/effect/.changeset/pre/remove-msgpack.md diff --git a/repos/effect/.changeset/pre/rename-schema-arbitrary-filter-constraint.md b/repos/effect/.changeset/pre/rename-schema-arbitrary-filter-constraint.md new file mode 100644 index 000000000..be77fbf9d --- /dev/null +++ b/repos/effect/.changeset/pre/rename-schema-arbitrary-filter-constraint.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Rename `Schema.Annotations.ToArbitrary.Constraint` to `Schema.Annotations.ToArbitrary.FilterConstraint`. + +Code that refers to the previous type name should update its type annotations to use `FilterConstraint`. diff --git a/repos/effect/.changeset/pre/rename-schema-transform-effect.md b/repos/effect/.changeset/pre/rename-schema-transform-effect.md new file mode 100644 index 000000000..273dfb7e4 --- /dev/null +++ b/repos/effect/.changeset/pre/rename-schema-transform-effect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename `SchemaGetter.transformOrFail` to `SchemaGetter.transformEffect` and `SchemaTransformation.transformOrFail` to `SchemaTransformation.transformEffect`. Replace calls to the old names with their `transformEffect` equivalents. diff --git a/repos/effect/.changeset/pre/repeat-or-else-metadata-argument.md b/repos/effect/.changeset/pre/repeat-or-else-metadata-argument.md new file mode 100644 index 000000000..1e0828ffa --- /dev/null +++ b/repos/effect/.changeset/pre/repeat-or-else-metadata-argument.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Correct the `Effect.repeatOrElse` fallback type to expose the previous step's `Schedule.Metadata`, matching the existing runtime value. diff --git a/repos/effect/.changeset/pre/report-mcp-tool-failures.md b/repos/effect/.changeset/pre/report-mcp-tool-failures.md new file mode 100644 index 000000000..531baac79 --- /dev/null +++ b/repos/effect/.changeset/pre/report-mcp-tool-failures.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Report recovered MCP toolkit failures and defects to configured `ErrorReporter`s, including declared tool failures returned with `isError: true`. diff --git a/repos/effect/.changeset/pre/request-cache-cancellation.md b/repos/effect/.changeset/pre/request-cache-cancellation.md new file mode 100644 index 000000000..9feb720d7 --- /dev/null +++ b/repos/effect/.changeset/pre/request-cache-cancellation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RequestResolver.withCache` retaining abandoned entries when a pending request is cancelled diff --git a/repos/effect/.changeset/pre/request-persisted-resolver-failures.md b/repos/effect/.changeset/pre/request-persisted-resolver-failures.md new file mode 100644 index 000000000..3268c2646 --- /dev/null +++ b/repos/effect/.changeset/pre/request-persisted-resolver-failures.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve completed results and propagate resolver failures from `RequestResolver.persisted`. diff --git a/repos/effect/.changeset/pre/request-race-cache.md b/repos/effect/.changeset/pre/request-race-cache.md new file mode 100644 index 000000000..2ae0bcc17 --- /dev/null +++ b/repos/effect/.changeset/pre/request-race-cache.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep completed results in `RequestResolver.withCache` when a losing `RequestResolver.race` resolver is interrupted after the winner completes, avoiding repeated backend requests on subsequent equal lookups. diff --git a/repos/effect/.changeset/pre/request-resolver-failure-causes.md b/repos/effect/.changeset/pre/request-resolver-failure-causes.md new file mode 100644 index 000000000..a77da246a --- /dev/null +++ b/repos/effect/.changeset/pre/request-resolver-failure-causes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve typed errors, defects, and interrupts from `RequestResolver.fromEffectTagged` handlers. diff --git a/repos/effect/.changeset/pre/request-resolver-iterable-results.md b/repos/effect/.changeset/pre/request-resolver-iterable-results.md new file mode 100644 index 000000000..59f70109e --- /dev/null +++ b/repos/effect/.changeset/pre/request-resolver-iterable-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RequestResolver.fromEffectTagged` to consume handler results as an iterable, allowing arrays, iterators, and generators to resolve requests in order. diff --git a/repos/effect/.changeset/pre/required-keys-index-signatures.md b/repos/effect/.changeset/pre/required-keys-index-signatures.md new file mode 100644 index 000000000..61a9a583d --- /dev/null +++ b/repos/effect/.changeset/pre/required-keys-index-signatures.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Types.RequiredKeys` dropping named required keys on types with index signatures. Derived type annotations may need to include these keys. diff --git a/repos/effect/.changeset/pre/resolved-client-urls.md b/repos/effect/.changeset/pre/resolved-client-urls.md new file mode 100644 index 000000000..b3554d530 --- /dev/null +++ b/repos/effect/.changeset/pre/resolved-client-urls.md @@ -0,0 +1,8 @@ +--- +"@effect/platform-browser": patch +"@effect/platform-node": patch +"effect": patch +--- + +Add `HttpClientResponse.url`, including query parameters and excluding the hash. When redirects are followed, it reports +the final URL. diff --git a/repos/effect/.changeset/pre/rpc-falsy-control-ids.md b/repos/effect/.changeset/pre/rpc-falsy-control-ids.md new file mode 100644 index 000000000..0c2d815d7 --- /dev/null +++ b/repos/effect/.changeset/pre/rpc-falsy-control-ids.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve zero and empty-string request IDs in JSON-RPC control messages. diff --git a/repos/effect/.changeset/pre/scheduler-global-scope-timers.md b/repos/effect/.changeset/pre/scheduler-global-scope-timers.md new file mode 100644 index 000000000..af24f0d77 --- /dev/null +++ b/repos/effect/.changeset/pre/scheduler-global-scope-timers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +The default scheduler falls back to a microtask when setting a timer throws. Cloudflare Workers disallow timers in global scope, so an effect that yielded while running at module load failed with "Disallowed operation called within global scope". diff --git a/repos/effect/.changeset/pre/schema-api-cleanup.md b/repos/effect/.changeset/pre/schema-api-cleanup.md new file mode 100644 index 000000000..b7a4c3d72 --- /dev/null +++ b/repos/effect/.changeset/pre/schema-api-cleanup.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Move the built-in schema revivers from `Schema` to `SchemaRepresentation`. +Rename the reviver constructors to `makeReviverDeclaration`, +`makeReviverFilter`, and `makeReviverFilterGroup`. + +Change `Schema.toEncoderXml` to fail with `SchemaIssue.Issue` directly instead +of wrapping failures in `SchemaError`. Consumers that read `error.issue` should +now use the error value itself. diff --git a/repos/effect/.changeset/pre/schema-array-leaf-aggregation.md b/repos/effect/.changeset/pre/schema-array-leaf-aggregation.md new file mode 100644 index 000000000..daf0561a8 --- /dev/null +++ b/repos/effect/.changeset/pre/schema-array-leaf-aggregation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve array-valued leaves when `SchemaGetter.makeTreeRecord` aggregates duplicate paths. diff --git a/repos/effect/.changeset/pre/schema-binary-preserve-bom.md b/repos/effect/.changeset/pre/schema-binary-preserve-bom.md new file mode 100644 index 000000000..2248164f1 --- /dev/null +++ b/repos/effect/.changeset/pre/schema-binary-preserve-bom.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve leading U+FEFF characters in SchemaBinary string values when decoding. diff --git a/repos/effect/.changeset/pre/schema-interpreter-backports.md b/repos/effect/.changeset/pre/schema-interpreter-backports.md new file mode 100644 index 000000000..87da660f4 --- /dev/null +++ b/repos/effect/.changeset/pre/schema-interpreter-backports.md @@ -0,0 +1,23 @@ +--- +"effect": patch +--- + +Align Schema construction and parsing semantics, simplify parse options, accept inherited declared fields, and move Union settings into a node-local options object. + +### Breaking changes + +- `Class.make`, `Class.makeOption`, and `Class.makeEffect` now return an existing instance unchanged. This avoids duplicate initialization and makes the construction APIs consistent. Use `new MyClass(input)` when a distinct instance is required. + +- `Literal(0)` and `Literal(-0)` continue to accept either signed zero, but decoding and encoding now preserve the input sign. Add an explicit transformation when a canonical sign is required. + +- `parseOptions` annotations no longer affect parsing. Options passed when creating or calling a decoder, encoder, or constructor adapter now apply to the complete operation. Move operation-wide settings from annotations to the relevant parser API. + +- `propertyOrder` has been removed from `ParseOptions` because preserving input order required a separate, rarely used object reconstruction path. Schema parsing no longer guarantees that decoded object keys follow their input order. Remove the option and apply any required presentation or serialization order after parsing. + +- `concurrency` now applies only to product children: tuple elements, array elements, struct fields, record entries, and structs with rest. It follows `Effect.forEach` semantics, defaults to sequential execution, and applies independently at every nested product. Union candidates remain sequential because speculative candidate evaluation can run transformations that are not selected. Existing product parsing can keep the option. Replace code that relied on concurrent Union candidates with explicitly coordinated parser calls. With concurrent Record key transformations, completion order determines the retained value when transformed keys collide. + +- `onExcessProperty: "preserve"` has been removed because it allowed unvalidated values absent from the schema type to cross the parsing boundary. Model additional properties with `Record` or `StructWithRest`; `"ignore"` and `"error"` remain available. + +- Declared `Struct` fields may now be inherited and are copied to own properties in the output. Dynamic `Record` index signatures remain own-only, while finite literal record keys are declared and may be inherited. The `__proto__` field remains own-only. Check ownership before parsing when every declared field must be own. + +- `SchemaAST.Union.mode` moved to `SchemaAST.Union.options?.mode` so node-local constructor settings live in one options object instead of special top-level fields. An absent value defaults to `"anyOf"`. `SchemaRepresentation.Union` now serializes `{ options: { mode: "oneOf" } }`; update direct AST access and regenerate or migrate persisted representation documents. The public `Schema.Union(members, { mode })` call is unchanged. diff --git a/repos/effect/.changeset/pre/schema-json-schema-options.md b/repos/effect/.changeset/pre/schema-json-schema-options.md new file mode 100644 index 000000000..e358933bc --- /dev/null +++ b/repos/effect/.changeset/pre/schema-json-schema-options.md @@ -0,0 +1,17 @@ +--- +"effect": patch +--- + +JSON Schema generation now follows the canonical JSON codec more closely and leaves unmodeled object properties open by default, matching Effect decoding. + +### Breaking changes + +`Schema.ToJsonSchemaOptions.additionalProperties` has been replaced by `onExcessProperty`: + +- Replace `{ additionalProperties: true }` with `{ onExcessProperty: "ignore" }`. +- Replace `{ additionalProperties: false }` with `{ onExcessProperty: "error" }`. +- Replace a schema-valued `additionalProperties` option with `Schema.Record` or `Schema.StructWithRest`. + +`Schema.Enum` now rejects non-finite numeric members. `Schema.isMultipleOf` now rejects zero and non-finite divisors, and normalizes negative divisors. + +Generation is more accurate for index signatures, empty structs, template literal alternatives, capitalized strings, and unique symbols. Conjunctive index keys remain open by default; `onExcessProperty: "error"` constrains them with `propertyNames`. diff --git a/repos/effect/.changeset/pre/scoped-cache-invalidate-all-reentrancy.md b/repos/effect/.changeset/pre/scoped-cache-invalidate-all-reentrancy.md new file mode 100644 index 000000000..2f7bc048a --- /dev/null +++ b/repos/effect/.changeset/pre/scoped-cache-invalidate-all-reentrancy.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `ScopedCache.invalidateAll` discarding entries created by reentrant resource finalizers without releasing them. Entries are now removed before their finalizers run, so replacement resources remain cached and are released when the cache closes. diff --git a/repos/effect/.changeset/pre/scoped-cache-refresh-lookup-defects.md b/repos/effect/.changeset/pre/scoped-cache-refresh-lookup-defects.md new file mode 100644 index 000000000..412dc00d9 --- /dev/null +++ b/repos/effect/.changeset/pre/scoped-cache-refresh-lookup-defects.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Capture synchronous defects thrown by `ScopedCache.refresh` lookup callbacks. diff --git a/repos/effect/.changeset/pre/scoped-log-nan-restoration.md b/repos/effect/.changeset/pre/scoped-log-nan-restoration.md new file mode 100644 index 000000000..07413320b --- /dev/null +++ b/repos/effect/.changeset/pre/scoped-log-nan-restoration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.annotateLogsScoped` to restore or remove unchanged `NaN` annotations when the scope closes. diff --git a/repos/effect/.changeset/pre/select-prompt-optional-message.md b/repos/effect/.changeset/pre/select-prompt-optional-message.md new file mode 100644 index 000000000..1165109f1 --- /dev/null +++ b/repos/effect/.changeset/pre/select-prompt-optional-message.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make `message` optional for `Prompt.Select` and `Prompt.MultiSelect`. When omitted, prompts display only the choices and submission shows a tick followed by the selected titles. `Prompt.AutoComplete` still requires a message. diff --git a/repos/effect/.changeset/pre/service-interface-const.md b/repos/effect/.changeset/pre/service-interface-const.md new file mode 100644 index 000000000..3018bd030 --- /dev/null +++ b/repos/effect/.changeset/pre/service-interface-const.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use branded interfaces for `Reactivity`, `LanguageModel`, `EmbeddingModel`, and `Chat`. Refer to each service's same-name type instead of `.Service` or `["Service"]`; custom implementations must include `[TypeId]: TypeId`. diff --git a/repos/effect/.changeset/pre/shaky-terms-push.md b/repos/effect/.changeset/pre/shaky-terms-push.md new file mode 100644 index 000000000..9e004ee1e --- /dev/null +++ b/repos/effect/.changeset/pre/shaky-terms-push.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix HttpRouter nested prefixed application order diff --git a/repos/effect/.changeset/pre/sharding-registration-context-overrides.md b/repos/effect/.changeset/pre/sharding-registration-context-overrides.md new file mode 100644 index 000000000..67c74a7d1 --- /dev/null +++ b/repos/effect/.changeset/pre/sharding-registration-context-overrides.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor services explicitly supplied when registering cluster entities while retaining construction-context services as fallbacks. diff --git a/repos/effect/.changeset/shy-seals-smile.md b/repos/effect/.changeset/pre/shy-seals-smile.md similarity index 100% rename from repos/effect/.changeset/shy-seals-smile.md rename to repos/effect/.changeset/pre/shy-seals-smile.md diff --git a/repos/effect/.changeset/pre/skip-disabled-stack-capture.md b/repos/effect/.changeset/pre/skip-disabled-stack-capture.md new file mode 100644 index 000000000..cbdf29e4f --- /dev/null +++ b/repos/effect/.changeset/pre/skip-disabled-stack-capture.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Skip optional stack capture when `Error.stackTraceLimit` is zero. diff --git a/repos/effect/.changeset/pre/small-basic-bundle.md b/repos/effect/.changeset/pre/small-basic-bundle.md new file mode 100644 index 000000000..eef21766d --- /dev/null +++ b/repos/effect/.changeset/pre/small-basic-bundle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reduce the basic Effect bundle size by keeping cause deduplication local, making encoding lookup tables tree-shakeable, removing redundant cause field declarations, and simplifying primitive hash dispatch without changing hash values. diff --git a/repos/effect/.changeset/pre/socket-paused-websocket-handoff.md b/repos/effect/.changeset/pre/socket-paused-websocket-handoff.md new file mode 100644 index 000000000..b8ee1aaef --- /dev/null +++ b/repos/effect/.changeset/pre/socket-paused-websocket-handoff.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Resume paused WebSockets after their readers take ownership. diff --git a/repos/effect/.changeset/pre/socket-websocket-text-byte-watermark.md b/repos/effect/.changeset/pre/socket-websocket-text-byte-watermark.md new file mode 100644 index 000000000..bd35e74b5 --- /dev/null +++ b/repos/effect/.changeset/pre/socket-websocket-text-byte-watermark.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Count buffered WebSocket text frames by their UTF-8 byte length when enforcing `highWaterMark`. diff --git a/repos/effect/.changeset/pre/split-lines-completed-cr.md b/repos/effect/.changeset/pre/split-lines-completed-cr.md new file mode 100644 index 000000000..5a3608f58 --- /dev/null +++ b/repos/effect/.changeset/pre/split-lines-completed-cr.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Emit CR-terminated lines from `Stream.splitLines` without pulling upstream again. diff --git a/repos/effect/.changeset/spotty-masks-own.md b/repos/effect/.changeset/pre/spotty-masks-own.md similarity index 100% rename from repos/effect/.changeset/spotty-masks-own.md rename to repos/effect/.changeset/pre/spotty-masks-own.md diff --git a/repos/effect/.changeset/pre/sql-event-journal-callback-errors.md b/repos/effect/.changeset/pre/sql-event-journal-callback-errors.md new file mode 100644 index 000000000..2d3016175 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-event-journal-callback-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve callback error identity in `SqlEventJournal.write` and `SqlEventJournal.withRemoteUncommited`. diff --git a/repos/effect/.changeset/pre/sql-message-storage-joined-reply-id.md b/repos/effect/.changeset/pre/sql-message-storage-joined-reply-id.md new file mode 100644 index 000000000..37a3db749 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-message-storage-joined-reply-id.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve reply IDs in SQL-backed `MessageStorage.unprocessedMessagesById` reads. diff --git a/repos/effect/.changeset/pre/sql-nested-placeholder-cache.md b/repos/effect/.changeset/pre/sql-nested-placeholder-cache.md new file mode 100644 index 000000000..978826051 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-nested-placeholder-cache.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix placeholder numbering for cached fragments used in returning helpers. diff --git a/repos/effect/.changeset/pre/sql-optional-span-propagation.md b/repos/effect/.changeset/pre/sql-optional-span-propagation.md new file mode 100644 index 000000000..f925c1ad0 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-optional-span-propagation.md @@ -0,0 +1,12 @@ +--- +"effect": patch +--- + +Add `Statement.SpanPropagationEnabled` to scope driver span parenting under `sql.execute` for any SQL client. Disabled by default. + +```ts +import { Effect } from "effect" +import { Statement } from "effect/unstable/sql" + +query.pipe(Effect.provideService(Statement.SpanPropagationEnabled, true)) +``` diff --git a/repos/effect/.changeset/pre/sql-pg-cancel-pending.md b/repos/effect/.changeset/pre/sql-pg-cancel-pending.md new file mode 100644 index 000000000..3c0967bf0 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-pg-cancel-pending.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Retire pooled PostgreSQL sessions with an unconfirmed `CancelRequest`, preventing a delayed cancel from reaching a later checkout. This includes interrupts sent while idle. The current checkout and unpooled sessions remain exposed to their own late cancel. diff --git a/repos/effect/.changeset/pre/sql-pg-timestamp-date.md b/repos/effect/.changeset/pre/sql-pg-timestamp-date.md new file mode 100644 index 000000000..7f90d4881 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-pg-timestamp-date.md @@ -0,0 +1,12 @@ +--- +"@effect/sql-pg": patch +--- + +Decode `timestamp` and `timestamptz` values, including array elements, as `Date` instead of epoch milliseconds. Their encoders, including `PgTypes.timestamp` and `PgTypes.timestamptz`, accept either form. Precision remains milliseconds. + +### Breaking changes + +- Numeric readers must call `date.getTime()` or restore numeric codecs with `PgTypes.register` or a client `Registry` passed as `types`. +- `infinity`, `-infinity` and values outside the JavaScript `Date` range decode to an invalid `Date`. Numeric `±Infinity` still encodes PostgreSQL's sentinels; encoding an invalid `Date` fails with `PgTypes.CodecError`. + +Date parameters bind as `timestamptz`. Inserting one into a `timestamp` column applies the session `TimeZone`. Use UTC or `PgTypes.timestamp(value)` to preserve its UTC fields. `timestamptz` round trips preserve the instant regardless of session timezone. diff --git a/repos/effect/.changeset/pre/sql-pg-unknown-oid-text.md b/repos/effect/.changeset/pre/sql-pg-unknown-oid-text.md new file mode 100644 index 000000000..1e9535968 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-pg-unknown-oid-text.md @@ -0,0 +1,9 @@ +--- +"@effect/sql-pg": patch +--- + +Decode unregistered OIDs as UTF-8 text so scalar enums return string labels. + +Other binary user-defined types, including enum arrays, may produce garbled text or fail UTF-8 decoding. Decode failures close the connection, failing its pending queries and discarding transactions and `LISTEN` subscriptions; the pool replaces it. + +Register scalar codecs with `PgTypes.register`. For arrays, use `PgTypes.makeRegistry().register(elementOid, codec, { arrayOid })` and pass the registry as the client's `types` option. diff --git a/repos/effect/.changeset/pre/sql-returning-identifier.md b/repos/effect/.changeset/pre/sql-returning-identifier.md new file mode 100644 index 000000000..7922dc865 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-returning-identifier.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix SQL returning helpers to compile identifiers with dialect-specific escaping. diff --git a/repos/effect/.changeset/pre/sql-runner-requested-shard-results.md b/repos/effect/.changeset/pre/sql-runner-requested-shard-results.md new file mode 100644 index 000000000..bbce8efe4 --- /dev/null +++ b/repos/effect/.changeset/pre/sql-runner-requested-shard-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure PostgreSQL shard acquisition and refresh return only the requested shards. diff --git a/repos/effect/.changeset/pre/sqlite-bun-transaction-export.md b/repos/effect/.changeset/pre/sqlite-bun-transaction-export.md new file mode 100644 index 000000000..52739714a --- /dev/null +++ b/repos/effect/.changeset/pre/sqlite-bun-transaction-export.md @@ -0,0 +1,6 @@ +--- +"@effect/sql-sqlite-bun": patch +--- + +Allow database exports inside `withTransaction` to complete instead of waiting indefinitely. The exported snapshot +includes the transaction's uncommitted writes, while exports outside that transaction still wait for it to finish. diff --git a/repos/effect/.changeset/pre/sqlite-do-native-completion.md b/repos/effect/.changeset/pre/sqlite-do-native-completion.md new file mode 100644 index 000000000..75867a216 --- /dev/null +++ b/repos/effect/.changeset/pre/sqlite-do-native-completion.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-do": patch +--- + +Propagate native Durable Object transaction completion failures as `SqlError`. diff --git a/repos/effect/.changeset/pre/sqlite-do-stream-errors.md b/repos/effect/.changeset/pre/sqlite-do-stream-errors.md new file mode 100644 index 000000000..af0b108b0 --- /dev/null +++ b/repos/effect/.changeset/pre/sqlite-do-stream-errors.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-do": patch +--- + +Report synchronous Durable Object SQL streaming failures as recoverable `SqlError` values. diff --git a/repos/effect/.changeset/pre/sqlite-node-unprepared-errors.md b/repos/effect/.changeset/pre/sqlite-node-unprepared-errors.md new file mode 100644 index 000000000..be4ae32fa --- /dev/null +++ b/repos/effect/.changeset/pre/sqlite-node-unprepared-errors.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-node": patch +--- + +Capture unprepared statement preparation failures as typed `SqlError`s. diff --git a/repos/effect/.changeset/pre/sqlite-wasm-messageport-startup.md b/repos/effect/.changeset/pre/sqlite-wasm-messageport-startup.md new file mode 100644 index 000000000..bd2530e4c --- /dev/null +++ b/repos/effect/.changeset/pre/sqlite-wasm-messageport-startup.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-wasm": patch +--- + +Start incoming MessagePort queues after registering the SQLite WASM client and OPFS worker listeners. Fresh ports, including a SharedWorker's client port, no longer require manual activation to receive ready messages and query replies or process worker requests. diff --git a/repos/effect/.changeset/pre/sqlite-wasm-statement-columns.md b/repos/effect/.changeset/pre/sqlite-wasm-statement-columns.md new file mode 100644 index 000000000..889b6cc7d --- /dev/null +++ b/repos/effect/.changeset/pre/sqlite-wasm-statement-columns.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-wasm": patch +--- + +Preserve statement-specific column names in SQLite WASM worker-backed query results. diff --git a/repos/effect/.changeset/pre/sqlite-wasm-worker-error-metadata.md b/repos/effect/.changeset/pre/sqlite-wasm-worker-error-metadata.md new file mode 100644 index 000000000..b4e0486cc --- /dev/null +++ b/repos/effect/.changeset/pre/sqlite-wasm-worker-error-metadata.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-wasm": patch +--- + +Preserve native SQLite error codes in OPFS worker replies so the client can classify constraint failures as `ConstraintError` instead of `UnknownError`. For coded worker failures, `SqlError.reason.cause` now contains a `{ message, code }` record instead of a string. diff --git a/repos/effect/.changeset/pre/sqlmodel-insert-decoding-services.md b/repos/effect/.changeset/pre/sqlmodel-insert-decoding-services.md new file mode 100644 index 000000000..58b1a15a3 --- /dev/null +++ b/repos/effect/.changeset/pre/sqlmodel-insert-decoding-services.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Include the model's decoding services in the public requirements of `SqlModel.makeResolvers().insert`, alongside its existing input-encoding services. + +This intentionally tightens compile-time checking: previously accepted callers must now provide the services already needed to decode inserted rows at runtime. Provide those services when executing the insert with `SqlResolver.request`. `insertVoid` still requires only input-encoding services, and service-free models need no changes. Runtime behavior is unchanged. diff --git a/repos/effect/.changeset/pre/stream-rechunk-large-source.md b/repos/effect/.changeset/pre/stream-rechunk-large-source.md new file mode 100644 index 000000000..f96dfb7d3 --- /dev/null +++ b/repos/effect/.changeset/pre/stream-rechunk-large-source.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.rechunk` failing on large source chunks. diff --git a/repos/effect/.changeset/pre/strict-byte-size-input.md b/repos/effect/.changeset/pre/strict-byte-size-input.md new file mode 100644 index 000000000..231a6d587 --- /dev/null +++ b/repos/effect/.changeset/pre/strict-byte-size-input.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Restrict `ByteSize.Input` strings to canonical non-negative integers with recognized units, rejecting malformed literals at compile time. Parse external strings and fractional quantities with `ByteSize.fromString` or `ByteSize.fromStringUnsafe` before passing them to APIs accepting `ByteSize.Input`. diff --git a/repos/effect/.changeset/strict-content-length.md b/repos/effect/.changeset/pre/strict-content-length.md similarity index 100% rename from repos/effect/.changeset/strict-content-length.md rename to repos/effect/.changeset/pre/strict-content-length.md diff --git a/repos/effect/.changeset/strict-cookie-names.md b/repos/effect/.changeset/pre/strict-cookie-names.md similarity index 100% rename from repos/effect/.changeset/strict-cookie-names.md rename to repos/effect/.changeset/pre/strict-cookie-names.md diff --git a/repos/effect/.changeset/pre/strict-mcp-tool-inputs.md b/repos/effect/.changeset/pre/strict-mcp-tool-inputs.md new file mode 100644 index 000000000..05d9deefc --- /dev/null +++ b/repos/effect/.changeset/pre/strict-mcp-tool-inputs.md @@ -0,0 +1,13 @@ +--- +"effect": patch +--- + +Honor `Tool.Strict` in MCP input schemas and argument validation. Strict dynamic tools require Effect schemas; raw JSON Schema is rejected at registration. + +Support identified input schemas for non-strict tools. Invalid arguments use `InvalidParams` on protocols before 2025-11-25 and `isError: true` results on newer protocols. + +Distinguish validation failures from declared handler failures. Declared failures return `isError: true` without `structuredContent`: error mode uses `Error.message` or schema-encoded text, and return mode uses the encoded payload. Declared failures do not produce internal-error diagnostics. + +Log and report internal failures, including defects and encoding errors, while keeping client messages generic. + +Allow `Toolkit.handle` to accept `SchemaAST.ParseOptions` for parameter decoding. Expose the `Toolkit.FailureOrigin` cause annotation and shared `Tool.FailureOrigin` type, with the same origin available in `Tool.HandlerResult.failureOrigin` on returned failures. diff --git a/repos/effect/.changeset/pre/struct-numeric-selection.md b/repos/effect/.changeset/pre/struct-numeric-selection.md new file mode 100644 index 000000000..7a947d2af --- /dev/null +++ b/repos/effect/.changeset/pre/struct-numeric-selection.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor numeric property selectors in Struct selection and mapping utilities. diff --git a/repos/effect/.changeset/pre/structural-schema-nodes.md b/repos/effect/.changeset/pre/structural-schema-nodes.md new file mode 100644 index 000000000..41d9897d2 --- /dev/null +++ b/repos/effect/.changeset/pre/structural-schema-nodes.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Expose `SchemaAST` nodes, `SchemaIssue` nodes, `SchemaGetter.Getter`, and the `SchemaTransformation` models through structural instance interfaces instead of concrete class declarations. The constructors remain usable with `new` and `instanceof`, but their `prototype` is no longer part of the public TypeScript API. Replace type-level access through a constructor's `prototype` with the corresponding named instance interface, such as `SchemaGetter.Getter`. + +`SchemaAST.Base` is no longer exported. Use `SchemaAST.AST` when accepting any AST node, and use the `SchemaAST.is*` guards to narrow individual variants. diff --git a/repos/effect/.changeset/pre/synchronized-ref-modify-some-effect-currying.md b/repos/effect/.changeset/pre/synchronized-ref-modify-some-effect-currying.md new file mode 100644 index 000000000..8691cfd97 --- /dev/null +++ b/repos/effect/.changeset/pre/synchronized-ref-modify-some-effect-currying.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the curried `SynchronizedRef.modifySomeEffect` overload to accept only the callback, matching its runtime behavior. diff --git a/repos/effect/.changeset/pre/synchronized-ref-not-ref.md b/repos/effect/.changeset/pre/synchronized-ref-not-ref.md new file mode 100644 index 000000000..13bee8c91 --- /dev/null +++ b/repos/effect/.changeset/pre/synchronized-ref-not-ref.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Stop declaring `SynchronizedRef` as a subtype of `Ref`, preventing `Ref` combinators from accepting values that do not implement the required runtime representation. diff --git a/repos/effect/.changeset/pre/tap-defect-saved-error-inference.md b/repos/effect/.changeset/pre/tap-defect-saved-error-inference.md new file mode 100644 index 000000000..9c85a3983 --- /dev/null +++ b/repos/effect/.changeset/pre/tap-defect-saved-error-inference.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve the source error type when a saved `Effect.tapDefect` operator is applied. The source error is now inferred from each application instead of when the operator is created. Runtime behavior is unchanged. diff --git a/repos/effect/.changeset/tcp-schema-binary-default.md b/repos/effect/.changeset/pre/tcp-schema-binary-default.md similarity index 100% rename from repos/effect/.changeset/tcp-schema-binary-default.md rename to repos/effect/.changeset/pre/tcp-schema-binary-default.md diff --git a/repos/effect/.changeset/pre/template-literal-parts-without-encoding.md b/repos/effect/.changeset/pre/template-literal-parts-without-encoding.md new file mode 100644 index 000000000..246f3b36a --- /dev/null +++ b/repos/effect/.changeset/pre/template-literal-parts-without-encoding.md @@ -0,0 +1,15 @@ +--- +"effect": patch +--- + +Separate template literal validation from transformed tuple parsing. `TemplateLiteralParser` now propagates its parts' decoding and encoding service requirements. + +### Breaking changes + +`Schema.TemplateLiteral` and `SchemaAST.TemplateLiteral` now throw during construction when a part contains an encoding, including inside unions and nested templates. This also rejects transformations whose decoded and encoded types are equal. Brands and supported checks without encodings remain valid. + +Use `Schema.Literals([0, 1])` to describe bit spellings or `Schema.Finite` to describe finite numeric spellings. Use `Schema.TemplateLiteralParser` when you need to decode transformed parts into a tuple. Explicit `Schema.toType` or `Schema.toEncoded` projections can remove an encoding, but do not necessarily preserve the strings accepted by the old template. For example, a `Finite` part rejects the empty segment accepted by `FiniteFromString`. + +`Schema.toEncoded(Schema.TemplateLiteralParser(...))` now validates the structure of the template instead of accepting any string. Use `Schema.String` when unrestricted strings are intended. + +When parser parts require services, provide those services to the corresponding decoding or encoding effect. These requirements were previously omitted from the parser's types. diff --git a/repos/effect/.changeset/pre/testschema-own-field-asts.md b/repos/effect/.changeset/pre/testschema-own-field-asts.md new file mode 100644 index 000000000..42b34e752 --- /dev/null +++ b/repos/effect/.changeset/pre/testschema-own-field-asts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `TestSchema.Asserts.ast.fields.equals` to compare ASTs for all own struct fields, including symbol and non-enumerable keys. Equivalent field schemas now compare equally regardless of schema instance identity, while differing ASTs and distinct symbol keys remain unequal. diff --git a/repos/effect/.changeset/pre/thin-ends-hug.md b/repos/effect/.changeset/pre/thin-ends-hug.md new file mode 100644 index 000000000..e3d613b8a --- /dev/null +++ b/repos/effect/.changeset/pre/thin-ends-hug.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +dispatch websocket events directly diff --git a/repos/effect/.changeset/pre/tidy-apis-parse.md b/repos/effect/.changeset/pre/tidy-apis-parse.md new file mode 100644 index 000000000..cf51526b5 --- /dev/null +++ b/repos/effect/.changeset/pre/tidy-apis-parse.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Add `HttpApi.ParseOptions` to configure server and client codecs at the API, group, or endpoint level. + +`Sse.decodeSchema` and `ChannelSchema.decode` accept parse options, and `Schema.Cause` encodes reasons to their wire fields. + +SSE decoding omits absent IDs, including with default options. Use `Schema.optional(Schema.String)` instead of `Schema.UndefinedOr(Schema.String)` for IDs. With `onExcessProperty: "error"`, declare `event` (default: `"message"`) and any `id`, including inherited IDs. + +`HttpApiSchema.StreamSse` data-mode types and OpenAPI schemas now make `id` optional. diff --git a/repos/effect/.changeset/pre/tidy-lobsters-count.md b/repos/effect/.changeset/pre/tidy-lobsters-count.md new file mode 100644 index 000000000..8af9e44a1 --- /dev/null +++ b/repos/effect/.changeset/pre/tidy-lobsters-count.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Improve Schema-derived BigInt and BigDecimal arbitraries with wider magnitude, precision, and exponent coverage and numeric boundary shrinking. + +Cover intermediate magnitudes up to large one-sided BigInt bounds, handle BigDecimal bounds with widely different scales, and refine decimal counterexamples between simple values. diff --git a/repos/effect/.changeset/pre/tidy-panthers-smile.md b/repos/effect/.changeset/pre/tidy-panthers-smile.md new file mode 100644 index 000000000..b07928251 --- /dev/null +++ b/repos/effect/.changeset/pre/tidy-panthers-smile.md @@ -0,0 +1,8 @@ +--- +"effect": patch +--- + +Add generic branded NetAddress classifications and named validating schemas. +Singleton address constants retain their base-family types; guards expose +classifications on demand. Operations that derive new address bits return only +the IPv4 or IPv6 family until callers revalidate the classification. diff --git a/repos/effect/.changeset/tidy-parts-narrow.md b/repos/effect/.changeset/pre/tidy-parts-narrow.md similarity index 100% rename from repos/effect/.changeset/tidy-parts-narrow.md rename to repos/effect/.changeset/pre/tidy-parts-narrow.md diff --git a/repos/effect/.changeset/pre/tidy-probability-criteria.md b/repos/effect/.changeset/pre/tidy-probability-criteria.md new file mode 100644 index 000000000..43416ca46 --- /dev/null +++ b/repos/effect/.changeset/pre/tidy-probability-criteria.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Make `criteria` optional for `Decision.probability`. When supplied, `criteria` still requires descriptions for both `false` and `true`. + +`Decision.Probability.criteria` is now optional, so consumers reading outcome descriptions (for example, `decision.criteria.true`) must first guard against `undefined`. diff --git a/repos/effect/.changeset/tidy-tools-finish.md b/repos/effect/.changeset/pre/tidy-tools-finish.md similarity index 100% rename from repos/effect/.changeset/tidy-tools-finish.md rename to repos/effect/.changeset/pre/tidy-tools-finish.md diff --git a/repos/effect/.changeset/timeout-error-message.md b/repos/effect/.changeset/pre/timeout-error-message.md similarity index 100% rename from repos/effect/.changeset/timeout-error-message.md rename to repos/effect/.changeset/pre/timeout-error-message.md diff --git a/repos/effect/.changeset/pre/timeout-or-else-cleanup-before-fallback.md b/repos/effect/.changeset/pre/timeout-or-else-cleanup-before-fallback.md new file mode 100644 index 000000000..02eb8f720 --- /dev/null +++ b/repos/effect/.changeset/pre/timeout-or-else-cleanup-before-fallback.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `Effect.timeoutOrElse` to finish interrupting the source before evaluating the fallback, preventing the source from winning after the timeout. + +Fallbacks now run in the caller fiber and inherit its interruptibility and supervision. diff --git a/repos/effect/.changeset/pre/toclient-raw-response-readable.md b/repos/effect/.changeset/pre/toclient-raw-response-readable.md new file mode 100644 index 000000000..dd8307783 --- /dev/null +++ b/repos/effect/.changeset/pre/toclient-raw-response-readable.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix repeated `text`, `json`, `arrayBuffer`, and `urlParamsBody` reads in `HttpServerResponse.toClientResponse` for raw Web `Response` bodies, preserving the original response for serving. diff --git a/repos/effect/.changeset/pre/token-bucket-elapsed-refill.md b/repos/effect/.changeset/pre/token-bucket-elapsed-refill.md new file mode 100644 index 000000000..6ee54efc4 --- /dev/null +++ b/repos/effect/.changeset/pre/token-bucket-elapsed-refill.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix token-bucket `retryAfter`, `delay` and `resetAfter` in the memory and Redis stores. Timing now follows whole-token refill boundaries and accounts for elapsed time, including fractional token costs. Redis preserves signed fractional counts and keeps keys until capacity actually refills. + +`RateLimiterStore.tokenBucket` now returns `[remaining, elapsedMillis]` instead of `remaining`. Custom stores must return both values from the same atomic operation; see the `tokenBucket` docs for the contract. Returning `[remaining, 0]` keeps the old timing bug. diff --git a/repos/effect/.changeset/pre/tokenizer-whole-prompt-truncation.md b/repos/effect/.changeset/pre/tokenizer-whole-prompt-truncation.md new file mode 100644 index 000000000..6258e5a22 --- /dev/null +++ b/repos/effect/.changeset/pre/tokenizer-whole-prompt-truncation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Tokenizer.truncate` to account for token costs between messages. diff --git a/repos/effect/.changeset/pre/tool-result-branch-encoding.md b/repos/effect/.changeset/pre/tool-result-branch-encoding.md new file mode 100644 index 000000000..5c609b07a --- /dev/null +++ b/repos/effect/.changeset/pre/tool-result-branch-encoding.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Encode tool results with the schema for their known success or failure branch. diff --git a/repos/effect/.changeset/pre/track-mapped-error-domain.md b/repos/effect/.changeset/pre/track-mapped-error-domain.md new file mode 100644 index 000000000..3c4c9a2c3 --- /dev/null +++ b/repos/effect/.changeset/pre/track-mapped-error-domain.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.track(metric, mapper)` to reject source errors the mapper cannot handle. diff --git a/repos/effect/.changeset/pre/trie-remove-valued-prefix.md b/repos/effect/.changeset/pre/trie-remove-valued-prefix.md new file mode 100644 index 000000000..beabebf6c --- /dev/null +++ b/repos/effect/.changeset/pre/trie-remove-valued-prefix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve valued prefix nodes when removing a longer key from a `Trie`. diff --git a/repos/effect/.changeset/pre/try-direct-error-types.md b/repos/effect/.changeset/pre/try-direct-error-types.md new file mode 100644 index 000000000..659c45466 --- /dev/null +++ b/repos/effect/.changeset/pre/try-direct-error-types.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Correct the error types of `Effect.try` and `Effect.tryPromise`. Direct function forms retain `Cause.UnknownError`, while `{ try, catch }` options use the error type returned by `catch`. + +Explicit two-generic direct calls, union-valued arguments, and generic aliases that combine the two forms no longer compile. Use `{ try, catch }` with a real error mapper, or narrow a union before calling the constructor. + +Runtime behavior, callback arguments, and error mapping are unchanged. diff --git a/repos/effect/.changeset/pre/tuple-optional-evolve-result-types.md b/repos/effect/.changeset/pre/tuple-optional-evolve-result-types.md new file mode 100644 index 000000000..c770854fd --- /dev/null +++ b/repos/effect/.changeset/pre/tuple-optional-evolve-result-types.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Correct `Tuple.evolve` result types when a transform may be `undefined`. The result now includes both the transformed and unchanged element types, matching the existing runtime behavior. Accepted inputs and runtime behavior are unchanged. + +Code relying on the previous, incorrect result type must handle both outcomes. For example, a number-to-string transform that may be absent now produces `number | string`, so callers assuming a number-only result must adjust. diff --git a/repos/effect/.changeset/pre/typesafe-decision-provider.md b/repos/effect/.changeset/pre/typesafe-decision-provider.md new file mode 100644 index 000000000..05c27e8f6 --- /dev/null +++ b/repos/effect/.changeset/pre/typesafe-decision-provider.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-typesafe": patch +--- + +Add `@effect/ai-typesafe`, a `DecisionModel` provider for TypeSafe's System One API. diff --git a/repos/effect/.changeset/pre/undici-response-form-data.md b/repos/effect/.changeset/pre/undici-response-form-data.md new file mode 100644 index 000000000..21a307e9e --- /dev/null +++ b/repos/effect/.changeset/pre/undici-response-form-data.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Parse URL-encoded and multipart response bodies in the Undici HTTP client. diff --git a/repos/effect/.changeset/pre/unstable-http-schemas.md b/repos/effect/.changeset/pre/unstable-http-schemas.md new file mode 100644 index 000000000..809b62c34 --- /dev/null +++ b/repos/effect/.changeset/pre/unstable-http-schemas.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Move the Cookie, Cookies, Headers, and UrlParams schemas from `effect/unstable/http` to `effect/Schema`, including their record and JSON-field helper schemas. diff --git a/repos/effect/.changeset/pre/update-workspace-dependencies.md b/repos/effect/.changeset/pre/update-workspace-dependencies.md new file mode 100644 index 000000000..4aba61889 --- /dev/null +++ b/repos/effect/.changeset/pre/update-workspace-dependencies.md @@ -0,0 +1,7 @@ +--- +"@effect/docgen": patch +"@effect/doctest": patch +"@effect/sql-d1": patch +--- + +Update repository dependencies to their latest compatible releases. diff --git a/repos/effect/.changeset/upgraded-request-skips-http-response-write.md b/repos/effect/.changeset/pre/upgraded-request-skips-http-response-write.md similarity index 100% rename from repos/effect/.changeset/upgraded-request-skips-http-response-write.md rename to repos/effect/.changeset/pre/upgraded-request-skips-http-response-write.md diff --git a/repos/effect/.changeset/pre/urlparams-null-input.md b/repos/effect/.changeset/pre/urlparams-null-input.md new file mode 100644 index 000000000..274b0d09e --- /dev/null +++ b/repos/effect/.changeset/pre/urlparams-null-input.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `UrlParams.fromInput` to stringify `null` values. diff --git a/repos/effect/.changeset/pre/urlparams-setall-immutable.md b/repos/effect/.changeset/pre/urlparams-setall-immutable.md new file mode 100644 index 000000000..25422eb6f --- /dev/null +++ b/repos/effect/.changeset/pre/urlparams-setall-immutable.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent `UrlParams.setAll` from mutating reusable overrides. diff --git a/repos/effect/.changeset/pre/use-canonical-array-indices.md b/repos/effect/.changeset/pre/use-canonical-array-indices.md new file mode 100644 index 000000000..a3bda5d21 --- /dev/null +++ b/repos/effect/.changeset/pre/use-canonical-array-indices.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Treat only unpadded decimal integers from `0` through `4294967294` as array indices in environment-backed configuration and bracket-path decoding. This preserves numeric-looking object keys and prevents out-of-range environment keys from producing impossible array lengths. Bracket paths that intend to address arrays must use `[1]` instead of `[01]`. diff --git a/repos/effect/.changeset/violet-pugs-tickle.md b/repos/effect/.changeset/pre/violet-pugs-tickle.md similarity index 100% rename from repos/effect/.changeset/violet-pugs-tickle.md rename to repos/effect/.changeset/pre/violet-pugs-tickle.md diff --git a/repos/effect/.changeset/pre/vitest-await-timeout-finalizers.md b/repos/effect/.changeset/pre/vitest-await-timeout-finalizers.md new file mode 100644 index 000000000..1fbd8b6b0 --- /dev/null +++ b/repos/effect/.changeset/pre/vitest-await-timeout-finalizers.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Await asynchronous test and layer finalizers after an Effect test times out before starting the next test. diff --git a/repos/effect/.changeset/pre/vitest-five-migration.md b/repos/effect/.changeset/pre/vitest-five-migration.md new file mode 100644 index 000000000..349baff34 --- /dev/null +++ b/repos/effect/.changeset/pre/vitest-five-migration.md @@ -0,0 +1,16 @@ +--- +"@effect/vitest": patch +"@effect/doctest": patch +--- + +Require Vitest `>=5.0.0 <6.0.0` and Node.js `^22.12.0 || ^24.0.0 || >=26.0.0`. + +### Breaking changes + +- Replace `.sequential` and `{ sequential: true }` with `{ concurrent: false }`. +- Use `bench` from the test context and await `bench(name, fn).run()`. The top-level benchmark API is removed. +- Use `Assertion` or `Assertion, T>`. Define custom matchers through `vitest.Matchers`, not `@vitest/expect`. +- Import reporter types from `vitest/node` and environment/snapshot APIs from `vitest/runtime`. Set `outputFile` when consuming JSON reports. +- Await asynchronous assertions. Mock history now clears before each test. + +See the [Vitest migration guide](https://vitest.dev/guide/migration/) for removed types and other upstream changes. diff --git a/repos/effect/.changeset/pre/vitest-layer-concurrency.md b/repos/effect/.changeset/pre/vitest-layer-concurrency.md new file mode 100644 index 000000000..2b1922a26 --- /dev/null +++ b/repos/effect/.changeset/pre/vitest-layer-concurrency.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Add a `concurrent` option to named `layer` and `it.layer` suites. Omitted options and anonymous layers preserve inherited concurrency. diff --git a/repos/effect/.changeset/pre/warn-workflow-tags.md b/repos/effect/.changeset/pre/warn-workflow-tags.md new file mode 100644 index 000000000..62821c799 --- /dev/null +++ b/repos/effect/.changeset/pre/warn-workflow-tags.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Warn when conflicting cluster workflow definitions reuse a tag. diff --git a/repos/effect/.changeset/pre/with-error-reporting-effect-result.md b/repos/effect/.changeset/pre/with-error-reporting-effect-result.md new file mode 100644 index 000000000..f359ab076 --- /dev/null +++ b/repos/effect/.changeset/pre/with-error-reporting-effect-result.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Fix `Effect.withErrorReporting` to return an `Effect` instead of preserving input +subtypes such as `Exit`, whose subtype-specific fields are not present on the wrapper. diff --git a/repos/effect/.changeset/pre/worker-run-early-exit.md b/repos/effect/.changeset/pre/worker-run-early-exit.md new file mode 100644 index 000000000..a5d68a606 --- /dev/null +++ b/repos/effect/.changeset/pre/worker-run-early-exit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Worker.run` hanging uninterruptibly when a worker dies before the ready handshake. diff --git a/repos/effect/.changeset/pre/xhr-arraybuffer-readers.md b/repos/effect/.changeset/pre/xhr-arraybuffer-readers.md new file mode 100644 index 000000000..d3ce6d29e --- /dev/null +++ b/repos/effect/.changeset/pre/xhr-arraybuffer-readers.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Support text, JSON, and stream readers when the XHR response type is `"arraybuffer"`. diff --git a/repos/effect/.changeset/pre/yaml-scalar-sequence-conformance.md b/repos/effect/.changeset/pre/yaml-scalar-sequence-conformance.md new file mode 100644 index 000000000..bbef3bc91 --- /dev/null +++ b/repos/effect/.changeset/pre/yaml-scalar-sequence-conformance.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix YAML indentless sequences, multiline scalar folding, and comments. Reject unsupported compact nested sequences (`- - value`) and document streams. + +Previously accepted values such as `description: Use when: deploy` and `description: Deploy:` now throw `SyntaxError`: YAML forbids colons followed by whitespace or end-of-value in plain scalars. Quote these values, for example `description: "Use when: deploy"`. diff --git a/repos/effect/.github/actions/setup/action.yaml b/repos/effect/.github/actions/setup/action.yaml index bed189322..0f97b61a1 100644 --- a/repos/effect/.github/actions/setup/action.yaml +++ b/repos/effect/.github/actions/setup/action.yaml @@ -13,11 +13,19 @@ runs: steps: - name: Install pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + env: + npm_config_audit: "false" + npm_config_fund: "false" - name: Install node uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - cache: pnpm node-version: 26.4.0 + package-manager-cache: ${{ env.NSC_CONTAINER_REGISTRY == '' }} + - name: Configure pnpm cache + if: ${{ env.NSC_CONTAINER_REGISTRY != '' }} + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 + with: + cache: pnpm - name: Install deno uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 if: ${{ inputs.deno-version != '' }} diff --git a/repos/effect/.github/workflows/ai-codegen.yml b/repos/effect/.github/workflows/ai-codegen.yml index d46bec4e3..6a88b2827 100644 --- a/repos/effect/.github/workflows/ai-codegen.yml +++ b/repos/effect/.github/workflows/ai-codegen.yml @@ -15,7 +15,7 @@ jobs: codegen: name: AI Codegen if: github.repository_owner == 'Effect-Ts' - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small permissions: contents: write pull-requests: write diff --git a/repos/effect/.github/workflows/check.yml b/repos/effect/.github/workflows/check.yml index bbb1c979d..5bbcfe849 100644 --- a/repos/effect/.github/workflows/check.yml +++ b/repos/effect/.github/workflows/check.yml @@ -13,8 +13,8 @@ concurrency: permissions: {} jobs: - lint: - name: Lint + static-checks: + name: Static runs-on: ubuntu-latest permissions: contents: read @@ -23,11 +23,23 @@ jobs: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - - run: pnpm lint + - name: Lint + run: pnpm lint + - name: Check circular dependencies + run: pnpm circular + - name: Generate AI documentation + run: pnpm ai-docgen + - name: Verify AI documentation is up-to-date + run: | + if [ -n "$(git status --short)" ]; then + git status --short + echo "Run 'pnpm ai-docgen' and commit generated changes." + exit 1 + fi types: name: Types - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small permissions: contents: read timeout-minutes: 10 @@ -35,7 +47,13 @@ jobs: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup + with: + deno-version: v2.9.4 - run: pnpm check + env: + # Leave memory headroom for the runner by collecting the Go heap earlier. + GOMEMLIMIT: 4GiB + - run: deno check . - run: pnpm test-types --target '>=5.9' build: @@ -53,23 +71,7 @@ jobs: run: | sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json - run: pnpm build - - types-deno: - name: Types on Deno - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Install dependencies - uses: ./.github/actions/setup - with: - deno-version: v2.9.4 - - name: Set strip internals config - run: | - sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json - - run: deno check . + - run: pnpm check-dist-types bundle: name: Bundle @@ -93,9 +95,10 @@ jobs: sed -i 's/"stripInternal": false/"stripInternal": true/' base/tsconfig.base.json - name: Build run: | - pnpm build & - cd base && pnpm install && pnpm build & - wait + pnpm build + cd base + pnpm install + pnpm build - name: Compare bundle size run: node ./packages/tools/bundle/src/bin.ts compare --base-dir base/packages/tools/bundle/fixtures - name: Upload stats artifact @@ -107,24 +110,36 @@ jobs: if-no-files-found: error test: - name: Test - runs-on: ubuntu-latest + name: Test (${{ matrix.runtime }}) + runs-on: namespace-profile-linux-default env: EFFECT_INTEGRATION_TESTS: "1" + # The ephemeral runner provides cleanup without a Ryuk sidecar. + TESTCONTAINERS_RYUK_DISABLED: "true" permissions: contents: read - timeout-minutes: 10 + timeout-minutes: 15 strategy: fail-fast: false matrix: - shard: [1/2, 2/2] - runtime: [Node, Deno] + include: + - runtime: Node + deno: "" + bun: "" + command: pnpm test --max-concurrency=10 + - runtime: Deno + deno: v2.9.4 + bun: "" + command: deno task test --max-concurrency=10 + - runtime: Bun + deno: "" + bun: 1.4.0 + command: bun run --bun vitest run --max-concurrency=10 steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Pre-pull test container images run: | - docker pull testcontainers/ryuk:0.14.0 & docker pull ghcr.io/tursodatabase/libsql-server:main & docker pull postgres:alpine & docker pull mysql:lts & @@ -133,40 +148,17 @@ jobs: docker pull redis:alpine & wait - - name: Install dependencies - if: matrix.runtime == 'Node' - uses: ./.github/actions/setup - - name: Test - if: matrix.runtime == 'Node' - run: pnpm test --shard ${{ matrix.shard }} - - - name: Install dependencies - if: matrix.runtime == 'Deno' - uses: ./.github/actions/setup - with: - deno-version: v2.9.4 - - name: Test - if: matrix.runtime == 'Deno' - run: deno task test --shard ${{ matrix.shard }} - - test-bun: - name: Test on Bun - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - name: Install dependencies uses: ./.github/actions/setup with: - bun-version: 1.3.13 + deno-version: ${{ matrix.deno }} + bun-version: ${{ matrix.bun }} - name: Test - run: bun node_modules/vitest/vitest.mjs run --project @effect/platform-bun + run: ${{ matrix.command }} doctest: - name: Documentation Tests - runs-on: ubuntu-latest + name: Test (Documentation) + runs-on: namespace-profile-linux-small permissions: contents: read timeout-minutes: 10 @@ -176,36 +168,3 @@ jobs: uses: ./.github/actions/setup - name: Test Documentation run: pnpm doctest - - ai-docgen: - name: AI Documentation Generation - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Install dependencies - uses: ./.github/actions/setup - - name: Generate AI Documentation - run: pnpm ai-docgen - - name: Verify AI Documentation is up-to-date - run: | - if [ -n "$(git status --short)" ]; then - git status --short - echo "Run 'pnpm ai-docgen' and commit generated changes." - exit 1 - fi - - circular: - name: Circular Dependencies - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - name: Install dependencies - uses: ./.github/actions/setup - - name: Check for circular dependencies - run: pnpm circular diff --git a/repos/effect/.github/workflows/cluster.yml b/repos/effect/.github/workflows/cluster.yml index 606ee8018..ba674aed4 100644 --- a/repos/effect/.github/workflows/cluster.yml +++ b/repos/effect/.github/workflows/cluster.yml @@ -7,22 +7,17 @@ permissions: {} jobs: test: name: Test - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small timeout-minutes: 30 env: EFFECT_CLUSTER_TESTS: "1" + # The ephemeral runner provides cleanup without a Ryuk sidecar. + TESTCONTAINERS_RYUK_DISABLED: "true" 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 diff --git a/repos/effect/.github/workflows/release-queue.yml b/repos/effect/.github/workflows/release-queue.yml index cbf7d2472..1235f5adf 100644 --- a/repos/effect/.github/workflows/release-queue.yml +++ b/repos/effect/.github/workflows/release-queue.yml @@ -27,7 +27,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 token: ${{ secrets.EFFECT_BOT_GH }} diff --git a/repos/effect/.github/workflows/snapshot.yml b/repos/effect/.github/workflows/snapshot.yml index 30fca3d8a..69e9e2528 100644 --- a/repos/effect/.github/workflows/snapshot.yml +++ b/repos/effect/.github/workflows/snapshot.yml @@ -15,7 +15,7 @@ permissions: {} jobs: approval-gate: if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small environment: fork steps: - run: echo "Fork PR approved by maintainer." @@ -27,7 +27,7 @@ jobs: !cancelled() && (needs.approval-gate.result == 'success' || needs.approval-gate.result == 'skipped') && github.repository_owner == 'Effect-Ts' - runs-on: ubuntu-latest + runs-on: namespace-profile-linux-small timeout-minutes: 10 steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 @@ -39,6 +39,10 @@ jobs: - name: Run codemods run: pnpm codemod - name: Build package + env: + # Leave memory for the runner and package builds on the 8 GB runner. + # The native TypeScript compiler uses Go's soft memory limit. + GOMEMLIMIT: 4GiB run: pnpm build - name: Create snapshot id: snapshot diff --git a/repos/effect/README.md b/repos/effect/README.md index 93973efa2..7d911b40d 100644 --- a/repos/effect/README.md +++ b/repos/effect/README.md @@ -50,6 +50,7 @@ This monorepo contains the core `effect` package alongside integration packages | [`@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-typesafe`](packages/ai/typesafe) | TypeSafe decision provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-typesafe) | | [`@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) | diff --git a/repos/effect/ai-docs/package.json b/repos/effect/ai-docs/package.json index d21c796cc..e679adecb 100644 --- a/repos/effect/ai-docs/package.json +++ b/repos/effect/ai-docs/package.json @@ -27,10 +27,10 @@ "@effect/sql-sqlite-wasm": "workspace:*", "@effect/vitest": "workspace:*", "effect": "workspace:*", - "hono": "^4.13.5", - "nodemailer": "^9.0.6" + "hono": "^4.13.8", + "nodemailer": "^10.0.10" }, "devDependencies": { - "@types/nodemailer": "^8.0.1" + "@types/nodemailer": "^8.0.2" } } diff --git a/repos/effect/ai-docs/src/70_cli/10_basics.ts b/repos/effect/ai-docs/src/70_cli/10_basics.ts index 9d33b9db5..3f4a40c46 100644 --- a/repos/effect/ai-docs/src/70_cli/10_basics.ts +++ b/repos/effect/ai-docs/src/70_cli/10_basics.ts @@ -10,7 +10,7 @@ import { Argument, Command, Flag } from "effect/unstable/cli" // You can define flags outside of commands and reuse them across multiple // commands. -const workspace = Flag.string("workspace").pipe( +const workspace = Flag.String("workspace").pipe( Flag.withAlias("w"), Flag.withDescription("Workspace to operate on"), Flag.withDefault("personal") @@ -21,7 +21,7 @@ const workspace = Flag.string("workspace").pipe( const tasks = Command.make("tasks").pipe( Command.withSharedFlags({ workspace, - verbose: Flag.boolean("verbose").pipe( + verbose: Flag.Boolean("verbose").pipe( Flag.withAlias("v"), Flag.withDescription("Print diagnostic output"), Flag.withDefault(false) @@ -41,17 +41,17 @@ const Email = Schema.String.pipe( const create = Command.make( "create", { - title: Argument.string("title").pipe( + title: Argument.String("title").pipe( Argument.withDescription("Task title"), // Reject empty titles at parse time, so the handler only ever sees // valid input Argument.withSchema(Schema.NonEmptyString) ), - priority: Flag.choice("priority", ["low", "normal", "high"]).pipe( + priority: Flag.Literals("priority", ["low", "normal", "high"]).pipe( Flag.withDescription("Priority for the new task"), Flag.withDefault("normal") ), - assignee: Flag.string("assignee").pipe( + assignee: Flag.String("assignee").pipe( Flag.withDescription("Email address of the person to assign"), Flag.withSchema(Email), Flag.optional @@ -88,11 +88,11 @@ const create = Command.make( const list = Command.make( "list", { - status: Flag.choice("status", ["open", "done", "all"]).pipe( + status: Flag.Literals("status", ["open", "done", "all"]).pipe( Flag.withDescription("Filter tasks by status"), Flag.withDefault("open") ), - json: Flag.boolean("json").pipe( + json: Flag.Boolean("json").pipe( Flag.withDescription("Print machine-readable output"), Flag.withDefault(false) ) diff --git a/repos/effect/jsdocs.config.json b/repos/effect/jsdocs.config.json index f31bd337c..6811e35b4 100644 --- a/repos/effect/jsdocs.config.json +++ b/repos/effect/jsdocs.config.json @@ -6,6 +6,7 @@ ], "exclude": [ "**/node_modules/**", + "packages/**/ai-docs/**", "packages/tools/**", "packages/**/src/index.ts", "packages/effect/src/StandardSchema.ts", diff --git a/repos/effect/migration/annotations/effect__Channel.yaml b/repos/effect/migration/annotations/effect__Channel.yaml index 2b8ea573b..a03978a15 100644 --- a/repos/effect/migration/annotations/effect__Channel.yaml +++ b/repos/effect/migration/annotations/effect__Channel.yaml @@ -173,11 +173,11 @@ 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." + replacement: "Channel.runDrain" + note: "Use runDrain to consume all emitted elements and return the channel's done value." "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." + note: "No direct scoped runner remains. Use toPull in the caller scope and recover Cause.Done; use 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." diff --git a/repos/effect/migration/annotations/effect__Effect.yaml b/repos/effect/migration/annotations/effect__Effect.yaml index 4a7ee9816..06d2eca07 100644 --- a/repos/effect/migration/annotations/effect__Effect.yaml +++ b/repos/effect/migration/annotations/effect__Effect.yaml @@ -20,6 +20,7 @@ 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: + include: true replacement: "Effect.annotateLogs" note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." effect/Effect#ap: @@ -528,7 +529,7 @@ effect/Effect#transposeMapOption: 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." + note: "Use the callback overload for Cause.UnknownError, or the object overload with try and catch to map failures to a custom error. The callback-only overload does not accept a custom error type parameter." 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." @@ -605,6 +606,7 @@ 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: + include: true replacement: "Effect.withLogSpan" note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." effect/Effect#withMaxOpsBeforeYield: @@ -655,3 +657,9 @@ effect/Effect#zipLeft: effect/Effect#zipRight: replacement: "Effect.andThen" note: "Sequence the Effects and retain the second result. Adapt arguments and imports to the v4 API." +"effect/Effect#repeatOrElse": + replacement: "Effect.repeatOrElse" + note: "The fallback receives Option> instead of Option. Read metadata.output for the previous schedule output and metadata.attempt for the attempt count. The error also includes schedule failures." +"effect/Effect#tryPromise": + replacement: "Effect.tryPromise" + note: "Use the callback overload for Cause.UnknownError, or the object overload with try and catch to map failures to a custom error. The callback-only overload does not accept a custom error type parameter." diff --git a/repos/effect/migration/annotations/effect__Effectable.yaml b/repos/effect/migration/annotations/effect__Effectable.yaml index a90d6864a..fe8066d27 100644 --- a/repos/effect/migration/annotations/effect__Effectable.yaml +++ b/repos/effect/migration/annotations/effect__Effectable.yaml @@ -3,7 +3,7 @@ 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." + note: "Still available; replace commit() with an asEffect() method returning the Effect. The intermediate v4 override property/getter is no longer supported." "effect/Effectable#CommitPrimitive": replacement: "new() => Effect.Effect" note: "The named constructor interface was removed; inline the constructor type when needed." @@ -24,7 +24,7 @@ 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." + note: "Use Class and migrate commit() to asEffect(); 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/repos/effect/migration/annotations/effect__Graph.yaml b/repos/effect/migration/annotations/effect__Graph.yaml index 4b1997f9d..1f58ee52e 100644 --- a/repos/effect/migration/annotations/effect__Graph.yaml +++ b/repos/effect/migration/annotations/effect__Graph.yaml @@ -6,10 +6,10 @@ 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." + note: "The mutable type remains but no longer shares a public base interface with immutable Graph; 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." + replacement: "none" + note: "The common graph protocol was removed. Use Graph.Graph or Graph.MutableGraph as appropriate and replace storage-field access with public graph query and mutation functions." "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/repos/effect/migration/annotations/effect__JSONSchema.yaml b/repos/effect/migration/annotations/effect__JSONSchema.yaml index 34d61b8f0..62a4840de 100644 --- a/repos/effect/migration/annotations/effect__JSONSchema.yaml +++ b/repos/effect/migration/annotations/effect__JSONSchema.yaml @@ -1,6 +1,6 @@ "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." + note: "Wrap a low-level AST with Schema.make, then generate a draft 2020-12 document. Generation defaults to onExcessProperty: 'ignore'; use 'error' for closed objects. JSON Schema validation is approximate and does not replace Effect decoding." example: "Schema.toJsonSchemaDocument(Schema.make(ast))" "effect/JSONSchema#JsonSchema7": replacement: "JsonSchema.JsonSchema" @@ -67,5 +67,5 @@ 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." + note: "Generate draft 2020-12, then call JsonSchema.toDocumentDraft07 when draft-07 output is required. Generation now defaults to onExcessProperty: 'ignore'; pass 'error' to retain closed-object output, and use the same option when decoding to reject excess properties. The earlier v4 additionalProperties generation option was removed. JSON Schema validation is approximate and does not replace Effect decoding." example: "JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema))" diff --git a/repos/effect/migration/annotations/effect__Layer.yaml b/repos/effect/migration/annotations/effect__Layer.yaml index def772ee9..925c52e31 100644 --- a/repos/effect/migration/annotations/effect__Layer.yaml +++ b/repos/effect/migration/annotations/effect__Layer.yaml @@ -168,7 +168,7 @@ note: "No version-mismatch log-level Reference or public replacement exists." "effect/Layer#tapErrorCause": replacement: "Layer.tapCause" - note: "The cause observer was renamed." + note: "The cause observer was renamed. Its callback must accept the source layer's full error cause; a callback narrowed to only part of the error union is rejected." "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." @@ -187,3 +187,6 @@ "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." +"effect/Layer#tapError": + replacement: "Layer.tapError" + note: "The observer must accept the source layer's full error union. Widen callbacks that handled only a subtype; saved curried observers preserve the source error type." diff --git a/repos/effect/migration/annotations/effect__LayerMap.yaml b/repos/effect/migration/annotations/effect__LayerMap.yaml index a87d5e2f5..12e986353 100644 --- a/repos/effect/migration/annotations/effect__LayerMap.yaml +++ b/repos/effect/migration/annotations/effect__LayerMap.yaml @@ -3,7 +3,7 @@ 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." + note: "Use layer instead of Default, layerNoDeps instead of DefaultWithoutDependencies, and contextEffect instead of runtime. Preloading does not remove acquisition errors from later lookups, which can reacquire expired or invalidated entries." "effect/LayerMap#Service.Context": replacement: "LayerMap.Service.Services" note: "The input-services extractor was renamed." diff --git a/repos/effect/migration/annotations/effect__Logger.yaml b/repos/effect/migration/annotations/effect__Logger.yaml index b8a4a8cf4..77a9957fb 100644 --- a/repos/effect/migration/annotations/effect__Logger.yaml +++ b/repos/effect/migration/annotations/effect__Logger.yaml @@ -54,7 +54,7 @@ effect/Logger#pretty: 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." + note: "Renamed to consolePretty. Remove the stderr option; provide Logger.LogToStderr with true to route TTY output to console.error. Colors, formatDate, and mode remain constructor options." effect/Logger#prettyLoggerDefault: replacement: "Logger.consolePretty()" note: "The prebuilt singleton became a constructor call." @@ -95,8 +95,8 @@ 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." + replacement: "custom Logger.make wrapper using options.fiber.cache.span" + note: "No transparent generic equivalent remains. Read span identity from options.fiber.cache.span 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." diff --git a/repos/effect/migration/annotations/effect__ParseResult.yaml b/repos/effect/migration/annotations/effect__ParseResult.yaml index 3feefd473..da9f26d0b 100644 --- a/repos/effect/migration/annotations/effect__ParseResult.yaml +++ b/repos/effect/migration/annotations/effect__ParseResult.yaml @@ -8,9 +8,12 @@ "effect/ParseResult#ArrayFormatterIssue": replacement: "StandardSchemaV1.FailureResult[\"issues\"][number]" note: "Use the Standard Schema issue shape returned by makeFormatterStandardSchemaV1." +"effect/ParseResult#Composite": + replacement: "SchemaIssue.Composite" + note: "Composite parse failures moved to SchemaIssue. The v4 constructor takes the failing AST and an array of nested issues; input is retained only when reportInput is enabled." "effect/ParseResult#DeclarationDecodeUnknown": replacement: "SchemaGetter.Getter" - note: "Custom declaration decoding now uses SchemaGetter values and Schema.declare annotations." + note: "Custom declaration decoding now uses SchemaGetter values and Schema.declare annotations. Getter is a tagged union, not a constructor; use transformOptionalEffect for an Option-to-Effect decoder. Use standalone SchemaGetter.run, map, and compose instead of instance methods." "effect/ParseResult#decodeEither": replacement: "Schema.decodeExit" note: "Either parsing was replaced by Exit parsing." @@ -68,6 +71,9 @@ "effect/ParseResult#orElse": replacement: "Effect.orElse" note: "Schema transformations now use Effect combinators." +"effect/ParseResult#Pointer": + replacement: "SchemaIssue.Pointer" + note: "Path-qualified failures moved to SchemaIssue. Construct them with the property path and nested issue; rejected input belongs to the nested issue when reportInput is enabled." "effect/ParseResult#parseError": replacement: "Schema.SchemaError" note: "Construct a SchemaError from a SchemaIssue.Issue." @@ -94,6 +100,9 @@ replacement: "SchemaIssue.defaultFormatter" note: "Use the default SchemaIssue string formatter." example: "SchemaIssue.defaultFormatter(issue)" +"effect/ParseResult#Transformation": + replacement: "SchemaIssue.Encoding" + note: "Transformation-stage failures are Encoding issues in v4. They retain the failing AST and nested issue; the old Encoded, Transformation, and Type kind discriminator was removed." "effect/ParseResult#try": replacement: "Effect.try" note: "Schema transformations now use Effect and map thrown errors to SchemaIssue values." diff --git a/repos/effect/migration/annotations/effect__Schema.yaml b/repos/effect/migration/annotations/effect__Schema.yaml index 69c83c30e..6be978903 100644 --- a/repos/effect/migration/annotations/effect__Schema.yaml +++ b/repos/effect/migration/annotations/effect__Schema.yaml @@ -27,7 +27,7 @@ effect/Schema#Annotations.Doc: 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. + note: "Use the v4 filter annotation shape. arbitraryConstraint uses Schema.Annotations.ToArbitrary.FilterConstraint; the earlier v4 ToArbitrary.Constraint name was removed." 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. @@ -312,7 +312,7 @@ effect/Schema#EndsWithSchemaId: 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. + note: "Rename the enum constructor and pass the enum object. Numeric members must be finite; NaN and positive or negative Infinity throw at construction." 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. @@ -660,7 +660,7 @@ effect/Schema#MinLengthSchemaId: 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. + note: "Rename the predicate to `isMultipleOf` and apply it with `Schema.check` or a schema's `check` method. The divisor must be finite and nonzero or construction throws RangeError; negative divisors are normalized to their absolute value." 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. @@ -1155,10 +1155,10 @@ effect/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. + note: Pass template literal parts as one array. Parts must not contain encodings, including inside unions and nested templates. Transformations whose decoded and encoded types are equal are also rejected. Use Schema.TemplateLiteralParser(parts) for transformed parts. effect/Schema#TemplateLiteralParser: - replacement: Schema.TemplateLiteralParser(schema.parts) - note: Create the template schema first and pass its `parts` property. + replacement: Schema.TemplateLiteralParser(parts) + note: Pass template literal parts directly as one array. Transformed parts are supported, and their decoding and encoding services are required in the corresponding direction. effect/Schema#TimeZone: replacement: Schema.TimeZoneFromString note: Use the string codec; v4 `TimeZone` is the self schema. @@ -1190,8 +1190,8 @@ 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. + replacement: "schema.pipe(Schema.decodeTo(target, { decode: SchemaGetter.transformEffect(...), encode: ... }))" + note: Replace the constructor with `decodeTo` and effectful `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. @@ -1278,7 +1278,7 @@ effect/Schema#Union: note: Pass union members as one array. effect/Schema#UniqueSymbolFromSelf: replacement: Schema.UniqueSymbol - note: Use the v4 unique-symbol schema constructor. + note: "Use the v4 unique-symbol schema constructor. JSON encoding is available only for globally registered symbols and uses the exact String(symbol) value; local symbols cannot be encoded as JSON." 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. diff --git a/repos/effect/migration/annotations/effect__SchemaAST.yaml b/repos/effect/migration/annotations/effect__SchemaAST.yaml index ab4978f15..043934cf5 100644 --- a/repos/effect/migration/annotations/effect__SchemaAST.yaml +++ b/repos/effect/migration/annotations/effect__SchemaAST.yaml @@ -1,6 +1,6 @@ "effect/SchemaAST#Annotated": - replacement: "SchemaAST.Base" - note: "All v4 AST nodes extend Base, which owns annotations, checks, encoding, and context." + replacement: "SchemaAST.AST" + note: "The public base type was removed. Use the AST union; every variant still exposes annotations, checks, encoding, and context." "effect/SchemaAST#annotations": replacement: "SchemaAST.annotate" note: "Use the v4 annotation helper and string-keyed Schema.Annotations." @@ -18,7 +18,7 @@ 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." + note: "Per-schema batching annotations were removed. They have no direct replacement. ParseOptions.concurrency controls product child parsing only; configure request batching separately." "effect/SchemaAST#BatchingAnnotationId": replacement: "none" note: "Symbol annotation IDs were removed and batching is no longer a schema annotation." @@ -43,6 +43,9 @@ "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#pick": + replacement: "none" + note: "The low-level AST picker was removed. Keep field selection at the Schema.Struct level with mapFields and Struct.pick, or discriminate and rebuild custom AST nodes explicitly." "effect/SchemaAST#composeTransformation": replacement: "SchemaAST.Encoding" note: "V4 transformations are SchemaAST.Link values in an encoding chain; compose by adding links with SchemaAST.decodeTo." @@ -50,11 +53,11 @@ 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." + replacement: "SchemaAST.ParseOptions" + note: "Per-schema concurrency annotations were removed. Pass concurrency in runtime ParseOptions when creating or calling a decoder, encoder, or constructor. It applies independently to each tuple, array, struct, record, or struct-with-rest node and does not make Union candidates concurrent." "effect/SchemaAST#ConcurrencyAnnotationId": - replacement: "Schema.Annotations.Bottom[\"parseOptions\"]" - note: "Symbol annotation IDs were removed; put concurrency inside the parseOptions annotation." + replacement: "SchemaAST.ParseOptions" + note: "The annotation ID was removed. Pass concurrency in runtime ParseOptions; product parsing follows Effect.forEach concurrency semantics while Union candidates remain sequential." "effect/SchemaAST#Declaration": replacement: "SchemaAST.Declaration" note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." @@ -93,7 +96,7 @@ 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." + note: "Renamed to Enum in the v4 AST model. Numeric enum members must be finite; NaN and positive or negative Infinity throw at construction." "effect/SchemaAST#EquivalenceAnnotation": replacement: "Schema.Annotations.ToEquivalence.Declaration" note: "Equivalence derivation annotations now use the toEquivalence key in Schema.Annotations." @@ -114,7 +117,7 @@ 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." + note: "Batching annotations were removed. ParseOptions.concurrency controls product child parsing only; configure request batching separately." "effect/SchemaAST#getBrandAnnotation": replacement: "SchemaAST.resolveAt(\"brands\")" note: "Resolve the string-keyed brands annotation." @@ -122,8 +125,8 @@ 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." + replacement: "SchemaAST.ParseOptions" + note: "Concurrency is no longer read from schema annotations. Pass it in runtime ParseOptions when creating or calling a parser. It applies to product children, not Union candidates." "effect/SchemaAST#getDecodingFallbackAnnotation": replacement: "none" note: "Fallbacks are encoding middleware in v4, not readable annotations; attach them with Schema.catchDecoding." @@ -161,8 +164,8 @@ 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." + replacement: "none" + note: "Parse options are no longer schema annotations. Pass options when creating or calling a decoder or encoder; there is no annotation-based override for nested schemas." "effect/SchemaAST#getPropertySignatures": replacement: "SchemaAST.Objects.propertySignatures" note: "Narrow to Objects and read propertySignatures directly." @@ -327,10 +330,10 @@ 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." + note: "Pass parsing options at runtime. onExcessProperty supports ignore or error, not preserve; model extra values with an explicit Record or StructWithRest. propertyOrder was removed. concurrency follows Effect.forEach semantics for tuple, array, struct, record, and struct-with-rest children, applies independently at each nested product, and does not make Union candidates concurrent. Output key order is unspecified, including in values passed to checks. Handle required presentation or serialization order explicitly outside the parser." "effect/SchemaAST#ParseOptionsAnnotationId": - replacement: "Schema.Annotations.Bottom[\"parseOptions\"]" - note: "Symbol annotation IDs were removed; use the parseOptions key." + replacement: "none" + note: "Parse options are no longer schema annotations. Pass options when creating or calling a decoder or encoder; there is no annotation-based override for nested schemas." "effect/SchemaAST#partial": replacement: "Schema.mapFields + Struct.map(Schema.optional)" note: "Partial object transforms moved to schema field transforms." @@ -378,7 +381,7 @@ 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." + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. Parts must not contain encodings, including inside unions or nested templates; use Schema.TemplateLiteralParser for transformed parts." "effect/SchemaAST#TemplateLiteralSpan": replacement: "SchemaAST.TemplateLiteral" note: "Template literal parts are represented directly as AST values in v4." @@ -396,7 +399,7 @@ 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." + note: "Use Arrays(isMutable, elements, rest, annotations?, checks?, encoding?, context?, encodingChecks?)." "effect/SchemaAST#Type": replacement: "SchemaAST.AST" note: "The tuple-element Type wrapper was removed; optionality and mutability moved to Context." @@ -411,7 +414,7 @@ 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." + note: "Use Objects(propertySignatures, indexSignatures, annotations?, checks?, encoding?, context?, encodingChecks?). Output key order is unspecified." "effect/SchemaAST#TypeLiteralTransformation": replacement: "SchemaAST.Encoding" note: "Object transformations are encoding links; use Schema.encodeKeys for key mappings." @@ -423,7 +426,7 @@ 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." + note: "Pass member ASTs and an optional options object: new SchemaAST.Union(types, { mode: 'oneOf' }). Read options?.mode ?? 'anyOf' instead of a direct mode field." "effect/SchemaAST#UniqueSymbol": replacement: "SchemaAST.UniqueSymbol" note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." diff --git a/repos/effect/migration/annotations/effect__SortedSet.yaml b/repos/effect/migration/annotations/effect__SortedSet.yaml index cc0e6e0c8..7e95f864c 100644 --- a/repos/effect/migration/annotations/effect__SortedSet.yaml +++ b/repos/effect/migration/annotations/effect__SortedSet.yaml @@ -58,3 +58,6 @@ "effect/SortedSet#some": replacement: "HashSet.some" note: "Run the predicate against the replacement HashSet; sort first only if traversal order has observable effects." +"effect/SortedSet#toggle": + replacement: "HashSet.has + HashSet.add / HashSet.remove" + note: "HashSet has no toggle; branch on membership and add or remove the element." diff --git a/repos/effect/migration/annotations/effect__Stream.yaml b/repos/effect/migration/annotations/effect__Stream.yaml index c2d861cd5..e4396a591 100644 --- a/repos/effect/migration/annotations/effect__Stream.yaml +++ b/repos/effect/migration/annotations/effect__Stream.yaml @@ -1,3 +1,39 @@ +effect/Stream#bind: + include: true + replacement: "Stream.bind" + note: "The input must be a record. Start from Stream.Do or use Stream.bindTo(name) to wrap a non-record element. The field name may now re-bind an existing key; the resulting record type is Simplify & Record>." +effect/Stream#bindEffect: + include: true + replacement: "Stream.bindEffect" + note: "The input must be a record. Start from Stream.Do or use Stream.bindTo(name) to wrap a non-record element. Existing keys may be re-bound, with the new value replacing the old field type. The callback still returns an Effect." +effect/Stream#catchTags: + include: true + replacement: "Stream.catchTags" + note: "Only keys present in the tagged error union are accepted; remove handlers for tags the stream cannot fail with. Untagged errors remain in the error channel." +effect/Stream#mapBoth: + include: true + replacement: "Stream.mapBoth" + note: "Rename the options onSuccess to onElement and onFailure to onError. The callbacks still transform emitted elements and typed errors respectively." +effect/Stream#partition: + include: true + replacement: "Stream.partition" + note: "Pass a Result-returning Filter instead of a boolean predicate or refinement; Filter.fromPredicate(predicate) adapts existing predicates. The tuple order is now [passes, fails], the reverse of v3 [excluded, satisfying]. Rename bufferSize to capacity (default 16; also accepts \"unbounded\"). Acquisition is scoped and does not fail with E; the returned streams carry E." +effect/Stream#scan: + include: true + replacement: "Stream.scan" + note: "Wrap the initial state in a thunk: Stream.scan(() => initial, step). The thunk is evaluated for each stream run; allocate mutable initial state inside it to avoid sharing state between runs." +effect/Stream#scanEffect: + include: true + replacement: "Stream.scanEffect" + note: "Wrap the initial state in a thunk: Stream.scanEffect(() => initial, step). It is evaluated for each stream run; the step still returns an Effect." +effect/Stream#runIntoPubSub: + include: true + replacement: "Stream.runIntoPubSub" + note: "Pass a PubSub of plain elements rather than Take wrappers. Both overloads return Effect, so handle stream failures on the returned Effect instead of expecting them as PubSub messages. The optional shutdownOnEnd flag controls PubSub shutdown." +effect/Stream#let: + include: true + replacement: "Stream.let" + note: "Retained in v4 and aligned with Effect.let: the field name may re-bind an existing key (the computed value replaces it) and the record type is Simplify & Record>." 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." @@ -29,8 +65,9 @@ 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." + include: true + replacement: "Stream.as" + note: "Retained in v4 with the same signature; replaces every element with the provided constant value." 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." @@ -392,6 +429,7 @@ 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: + include: true replacement: "Stream.transduce" note: "Unchanged name and Sink-based shape; chunks are plain arrays in v4." effect/Stream#unfoldChunk: diff --git a/repos/effect/migration/annotations/effect__SynchronizedRef.yaml b/repos/effect/migration/annotations/effect__SynchronizedRef.yaml index 5fef8a050..37db3e7ba 100644 --- a/repos/effect/migration/annotations/effect__SynchronizedRef.yaml +++ b/repos/effect/migration/annotations/effect__SynchronizedRef.yaml @@ -1,9 +1,9 @@ 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." + note: "The model remains but no longer extends Ref; read and update it through explicit SynchronizedRef operations. The curried v4 modifySomeEffect takes only the callback, which returns an Effect of [result, Option]; remove the v3 fallback and outer Option." effect/SynchronizedRef#SynchronizedRef.Variance: - replacement: "Ref.Ref.Variance" - note: "SynchronizedRef now inherits Ref variance instead of declaring a separate public variance marker." + replacement: "none" + note: "The public nested variance marker was removed. SynchronizedRef uses an internal brand in v4, so do not refer to a variance interface directly." effect/SynchronizedRef#SynchronizedRefTypeId: replacement: "none" note: "The SynchronizedRef type id is internal in v4; do not inspect or construct the brand directly." diff --git a/repos/effect/migration/annotations/effect__Types.yaml b/repos/effect/migration/annotations/effect__Types.yaml index cbd7331fa..95bb82323 100644 --- a/repos/effect/migration/annotations/effect__Types.yaml +++ b/repos/effect/migration/annotations/effect__Types.yaml @@ -10,6 +10,9 @@ "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#DeepMutable": + replacement: "Types.DeepMutable" + note: "Retained, but v4 preserves built-in objects, Effect data types, and other objects with methods or symbol-keyed properties while making surrounding structural data mutable." "effect/Types#Invariant": replacement: "Types.Invariant" note: "Unchanged invariant type helper." diff --git a/repos/effect/migration/annotations/effect__ai-openai__OpenAiTool.yaml b/repos/effect/migration/annotations/effect__ai-openai__OpenAiTool.yaml new file mode 100644 index 000000000..7cf5207b1 --- /dev/null +++ b/repos/effect/migration/annotations/effect__ai-openai__OpenAiTool.yaml @@ -0,0 +1,14 @@ +"@effect/ai-openai/OpenAiTool#CodeInterpreter": + replacement: "OpenAiTool.CodeInterpreter" + note: "Results now wrap outputs in an object with status and outputs, rather than returning the output array or null directly. Only completed is successful; other statuses produce failure results that preserve outputs." +"@effect/ai-openai/OpenAiTool#FileSearch": + replacement: "OpenAiTool.FileSearch" + note: "Results contain status, queries, and a required results field holding matches or null. Only completed is successful; other statuses produce failure results that preserve partial matches." +"@effect/ai-openai/OpenAiTool#WebSearch": + include: true + replacement: "OpenAiTool.WebSearch" + note: "Results now include action alongside status. Only completed is successful; other statuses produce failure results. Inspect isFailure before treating a result as a successful search. Inspect each search source by its type: URL sources have url, while API sources have name and no url." +"@effect/ai-openai/OpenAiTool#WebSearchPreview": + include: true + replacement: "OpenAiTool.WebSearchPreview" + note: "Results now include action alongside status. Only completed is successful; other statuses produce failure results. Inspect isFailure before treating a result as a successful search. Inspect each search source by its type: URL sources have url, while API sources have name and no url." diff --git a/repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml b/repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml index 81023018b..a41065273 100644 --- a/repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml +++ b/repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml @@ -12,4 +12,4 @@ 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." + note: "Still exported in v4; adapt to the regenerated client, revised request and response schemas, and the new streaming result tuple. Custom service implementations and mocks must also provide createDecisions, returning an Effect of [decoded DecisionsResponse, HttpClientResponse] with AiError failures." diff --git a/repos/effect/migration/annotations/effect__ai__AiError.yaml b/repos/effect/migration/annotations/effect__ai__AiError.yaml index d906cf83a..34d041d50 100644 --- a/repos/effect/migration/annotations/effect__ai__AiError.yaml +++ b/repos/effect/migration/annotations/effect__ai__AiError.yaml @@ -19,3 +19,6 @@ "@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." +"@effect/ai/AiError#HttpRequestDetails": + replacement: "AiError.HttpRequestDetails" + note: "Retained in effect/unstable/ai/AiError and also re-exported as Response.HttpRequestDetails. Hash is an optional string instead of Option, headers may contain Redacted strings, and method includes TRACE." diff --git a/repos/effect/migration/annotations/effect__ai__Chat.yaml b/repos/effect/migration/annotations/effect__ai__Chat.yaml new file mode 100644 index 000000000..ce5272505 --- /dev/null +++ b/repos/effect/migration/annotations/effect__ai__Chat.yaml @@ -0,0 +1,6 @@ +"@effect/ai/Chat#Chat": + replacement: "Chat.Chat" + note: "Use the Chat interface for implementations and the Context.Service value as the service key; it is no longer a class. Chat.empty and Chat.fromPrompt construct branded chat sessions." +"@effect/ai/Chat#Service": + replacement: "Chat.Chat" + note: "Service was renamed to Chat. Implementations require [Chat.TypeId]: Chat.TypeId; use Chat.empty or Chat.fromPrompt to create a session." diff --git a/repos/effect/migration/annotations/effect__ai__EmbeddingModel.yaml b/repos/effect/migration/annotations/effect__ai__EmbeddingModel.yaml index a4c065c0d..b92a1a18b 100644 --- a/repos/effect/migration/annotations/effect__ai__EmbeddingModel.yaml +++ b/repos/effect/migration/annotations/effect__ai__EmbeddingModel.yaml @@ -1,3 +1,9 @@ "@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." +"@effect/ai/EmbeddingModel#EmbeddingModel": + replacement: "EmbeddingModel.EmbeddingModel" + note: "Use the EmbeddingModel interface for implementations and the Context.Service value as the service key; it is no longer a class. Prefer EmbeddingModel.make to construct the branded implementation." +"@effect/ai/EmbeddingModel#Service": + replacement: "EmbeddingModel.EmbeddingModel" + note: "Service was renamed to EmbeddingModel. Implementations require [EmbeddingModel.TypeId]: EmbeddingModel.TypeId; EmbeddingModel.make supplies the brand." diff --git a/repos/effect/migration/annotations/effect__ai__LanguageModel.yaml b/repos/effect/migration/annotations/effect__ai__LanguageModel.yaml index 7436f1bf5..19b26391d 100644 --- a/repos/effect/migration/annotations/effect__ai__LanguageModel.yaml +++ b/repos/effect/migration/annotations/effect__ai__LanguageModel.yaml @@ -4,3 +4,9 @@ "@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." +"@effect/ai/LanguageModel#LanguageModel": + replacement: "LanguageModel.LanguageModel" + note: "Use the LanguageModel interface for implementations and the Context.Service value as the service key; it is no longer a class. Prefer LanguageModel.make to construct the branded implementation." +"@effect/ai/LanguageModel#Service": + replacement: "LanguageModel.LanguageModel" + note: "Service was renamed to LanguageModel. Implementations require [LanguageModel.TypeId]: LanguageModel.TypeId; LanguageModel.make supplies the brand." diff --git a/repos/effect/migration/annotations/effect__ai__McpSchema.yaml b/repos/effect/migration/annotations/effect__ai__McpSchema.yaml index 2c7a565d1..626955bfc 100644 --- a/repos/effect/migration/annotations/effect__ai__McpSchema.yaml +++ b/repos/effect/migration/annotations/effect__ai__McpSchema.yaml @@ -16,6 +16,9 @@ "@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#McpServerClient": + replacement: "McpSchema.McpRequestContext / McpSchema.McpServerClient" + note: "Use McpRequestContext for client identity, capabilities, and request metadata in protocol-neutral handlers. McpServerClient remains available for initialized stateful protocols and reverse client requests; it is not provided by the stateless 2026-07-28 adapter. Registration helpers exclude McpRequestContext from handler requirements, not McpServerClient." "@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." diff --git a/repos/effect/migration/annotations/effect__ai__McpServer.yaml b/repos/effect/migration/annotations/effect__ai__McpServer.yaml index 7ded9767a..a23e5401e 100644 --- a/repos/effect/migration/annotations/effect__ai__McpServer.yaml +++ b/repos/effect/migration/annotations/effect__ai__McpServer.yaml @@ -13,3 +13,24 @@ "@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." +"@effect/ai/McpServer#McpServer": + replacement: "McpServer.McpServer" + note: "The service is now a Context.Service. initializedClients and notificationsMailbox are no longer exposed. Registration callbacks use McpSchema.McpRequestContext; McpServerClient is only supplied for initialized stateful requests. Prefer registerToolkit, registerResource, and registerPrompt over implementing the registry shape directly." +"@effect/ai/McpServer#registerToolkit": + replacement: "McpServer.registerToolkit" + note: "Moved to effect/unstable/ai/McpServer. Handler requirements now exclude McpSchema.McpRequestContext instead of McpServerClient. Strict tools reject excess input properties; raw JSON Schema dynamic tools cannot use strict mode. Declared handler failures produce isError results, while parameter validation fails with InvalidParams." +"@effect/ai/McpServer#toolkit": + replacement: "McpServer.toolkit" + note: "Moved to effect/unstable/ai/McpServer. The registration layer supplies McpSchema.McpRequestContext to handlers instead of excluding McpServerClient from requirements. Strict tools reject excess properties and require an Effect Schema rather than raw dynamic JSON Schema." +"@effect/ai/McpServer#registerResource": + replacement: "McpServer.registerResource" + note: "Moved to effect/unstable/ai/McpServer. Resource and completion handler requirements now exclude McpSchema.McpRequestContext instead of McpServerClient; use the request context for protocol-neutral client metadata." +"@effect/ai/McpServer#resource": + replacement: "McpServer.resource" + note: "Moved to effect/unstable/ai/McpServer. The registration layer excludes McpSchema.McpRequestContext from resource and completion handler requirements instead of McpServerClient." +"@effect/ai/McpServer#registerPrompt": + replacement: "McpServer.registerPrompt" + note: "Moved to effect/unstable/ai/McpServer. Prompt decoding and handler requirements now exclude McpSchema.McpRequestContext instead of McpServerClient; use the request context for protocol-neutral client metadata." +"@effect/ai/McpServer#prompt": + replacement: "McpServer.prompt" + note: "Moved to effect/unstable/ai/McpServer. The registration layer excludes McpSchema.McpRequestContext from prompt decoding and handler requirements instead of McpServerClient." diff --git a/repos/effect/migration/annotations/effect__ai__Response.yaml b/repos/effect/migration/annotations/effect__ai__Response.yaml index 2a1351c12..886befa16 100644 --- a/repos/effect/migration/annotations/effect__ai__Response.yaml +++ b/repos/effect/migration/annotations/effect__ai__Response.yaml @@ -67,3 +67,6 @@ "@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." +"@effect/ai/Response#ToolResultPart": + replacement: "Response.ToolResultPart" + note: "The schema selects success or failure using isFailure rather than trying both result schemas. It returns Schema.Codec; supply decoding services when decoding and encoding services when encoding." diff --git a/repos/effect/migration/annotations/effect__ai__Tool.yaml b/repos/effect/migration/annotations/effect__ai__Tool.yaml index 5bd660841..e0fefb044 100644 --- a/repos/effect/migration/annotations/effect__ai__Tool.yaml +++ b/repos/effect/migration/annotations/effect__ai__Tool.yaml @@ -55,3 +55,9 @@ "@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." +"@effect/ai/Tool#Result": + replacement: "Tool.Result" + note: "The result includes success, declared failure, and execution-denied or execution-interrupted values in both failure modes. Return mode also includes AiError; handle these variants when narrowing results." +"@effect/ai/Tool#ResultEncoded": + replacement: "Tool.ResultEncoded" + note: "The encoded result includes execution-denied and execution-interrupted variants in both failure modes, plus encoded AiError in return mode." diff --git a/repos/effect/migration/annotations/effect__cli__Args.yaml b/repos/effect/migration/annotations/effect__cli__Args.yaml index f95db093c..c07e2b7fe 100644 --- a/repos/effect/migration/annotations/effect__cli__Args.yaml +++ b/repos/effect/migration/annotations/effect__cli__Args.yaml @@ -15,9 +15,9 @@ 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." + 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 })" + 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" @@ -35,11 +35,35 @@ replacement: "Argument.between" note: "Use the moved combinator; v4 validates bounds when constructing the parameter." "@effect/cli/Args#boolean": - replacement: "Flag.boolean / Argument.choiceWithValue" + 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#choice": + replacement: "Argument.Literals" + note: "Use the renamed constructor and pass the argument name explicitly." +"@effect/cli/Args#date": + replacement: "Argument.Date" + note: "Use the renamed constructor and pass the argument name explicitly." +"@effect/cli/Args#directory": + replacement: "Argument.Directory" + note: "Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement." +"@effect/cli/Args#file": + replacement: "Argument.File" + note: "Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement." "@effect/cli/Args#fileContent": - replacement: "Argument.file + Argument.mapEffect" + replacement: "Argument.File + Argument.mapEffect" note: "Parse a path and read it with FileSystem.readFile; no binary-content argument constructor remains." +"@effect/cli/Args#fileParse": + replacement: "Argument.FileParse" + note: "Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple." +"@effect/cli/Args#fileSchema": + replacement: "Argument.FileSchema" + note: "Pass the old format as an options field and use a v4 Schema constraint decoder." +"@effect/cli/Args#fileText": + replacement: "Argument.FileText" + note: "Use the renamed constructor; it returns content only." +"@effect/cli/Args#float": + replacement: "Argument.Finite" + note: "Use the renamed constructor; it rejects non-finite numbers." "@effect/cli/Args#getHelp": replacement: "none" note: "Per-argument help introspection was removed; Command generates help internally." @@ -55,24 +79,36 @@ "@effect/cli/Args#getUsage": replacement: "none" note: "The public Usage tree was removed; Command generates a usage string internally." +"@effect/cli/Args#integer": + replacement: "Argument.Int" + note: "Use the renamed constructor and pass the argument name explicitly." "@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#none": + replacement: "omit the config entry" + note: "V4 Argument.Never is an always-failing sentinel, not v3's empty successful argument set." "@effect/cli/Args#optional": replacement: "Argument.optional" note: "Use the moved combinator; it still returns Option." +"@effect/cli/Args#path": + replacement: "Argument.Path" + note: "Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement." +"@effect/cli/Args#redacted": + replacement: "Argument.Redacted" + note: "Use the renamed constructor and pass the argument name explicitly." "@effect/cli/Args#repeated": replacement: "Argument.variadic" note: "Renamed to variadic; pass optional min and max bounds." "@effect/cli/Args#secret": - replacement: "Argument.redacted" + 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." + 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." diff --git a/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml b/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml index 3c157a9c8..8dc872ab7 100644 --- a/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml +++ b/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml @@ -1,5 +1,5 @@ "@effect/cli/CommandDirective#builtIn": - replacement: "GlobalFlag.action" + replacement: "GlobalFlag.Action" note: "Define a custom action flag; v4 runners no longer return built-in directives." "@effect/cli/CommandDirective#BuiltIn": replacement: "GlobalFlag.Action" diff --git a/repos/effect/migration/annotations/effect__cli__Options.yaml b/repos/effect/migration/annotations/effect__cli__Options.yaml index 22a76c724..966f792e3 100644 --- a/repos/effect/migration/annotations/effect__cli__Options.yaml +++ b/repos/effect/migration/annotations/effect__cli__Options.yaml @@ -17,40 +17,40 @@ replacement: "Flag.between" note: "Use the moved combinator; v4 validates bounds when constructing the parameter." "@effect/cli/Options#boolean": - replacement: "Flag.boolean + Flag.withDefault" - note: "Use Flag.boolean(name).pipe(Flag.withDefault(false)) to preserve v3's omitted-flag default; bare Flag.boolean is now required. --no-name is automatic and aliases are added with Flag.withAlias." + replacement: "Flag.Boolean + Flag.withDefault" + note: "Use Flag.Boolean(name).pipe(Flag.withDefault(false)) to preserve v3's omitted-flag default; bare Flag.Boolean is now required. --no-name is automatic and aliases are added with Flag.withAlias." "@effect/cli/Options#choice": - replacement: "Flag.choice" + replacement: "Flag.Literals" note: "Use the moved constructor." "@effect/cli/Options#choiceWithValue": - replacement: "Flag.choiceWithValue" + replacement: "Flag.ChoiceWithValue" note: "Use the moved constructor." "@effect/cli/Options#date": - replacement: "Flag.date" + replacement: "Flag.Date" note: "Use the moved constructor." "@effect/cli/Options#directory": - replacement: "Flag.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" + 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" + 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" + 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" + 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." + 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" + replacement: "Flag.Finite" note: "Use the moved constructor." "@effect/cli/Options#getHelp": replacement: "none" @@ -62,7 +62,7 @@ replacement: "none" note: "The public Usage tree was removed; Command generates a usage string internally." "@effect/cli/Options#integer": - replacement: "Flag.integer" + replacement: "Flag.Int" note: "Use the moved constructor." "@effect/cli/Options#isBool": replacement: "none" @@ -71,7 +71,7 @@ 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" + replacement: "Flag.KeyValuePair" note: "Renamed and now returns Record rather than HashMap." "@effect/cli/Options#map": replacement: "Flag.map" @@ -84,7 +84,7 @@ 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." + note: "V4 Flag.Never 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." @@ -92,7 +92,7 @@ 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" + 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 }" @@ -116,17 +116,17 @@ 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" + 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" + 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." + replacement: "Flag.String" + note: "Renamed from text to String." "@effect/cli/Options#withAlias": replacement: "Flag.withAlias" note: "Use the moved combinator." diff --git a/repos/effect/migration/annotations/effect__cli__Primitive.yaml b/repos/effect/migration/annotations/effect__cli__Primitive.yaml index 35082dfc9..ab6eec519 100644 --- a/repos/effect/migration/annotations/effect__cli__Primitive.yaml +++ b/repos/effect/migration/annotations/effect__cli__Primitive.yaml @@ -1,15 +1,15 @@ "@effect/cli/Primitive#boolean": - replacement: "Primitive.boolean" - note: "Boolean is now a singleton value; defaults belong on Flag.boolean or withDefault." + replacement: "Primitive.Boolean" + note: "Boolean is now a singleton value; defaults belong on Flag.Boolean or withDefault." "@effect/cli/Primitive#choice": - replacement: "Primitive.choice" + replacement: "Primitive.Choice" note: "Use the moved constructor." "@effect/cli/Primitive#date": - replacement: "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." + replacement: "Primitive.Finite" + note: "Finite 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." @@ -17,8 +17,8 @@ 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." + replacement: "Primitive.Int" + note: "Int is now a singleton Primitive value." "@effect/cli/Primitive#isBool": replacement: "none" note: "The boolean Primitive predicate is internal in v4." @@ -35,8 +35,8 @@ replacement: "none" note: "The public Primitive type-id symbol was removed." "@effect/cli/Primitive#text": - replacement: "Primitive.string" - note: "Renamed from text to string." + 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/repos/effect/migration/annotations/effect__cli__Prompt.yaml b/repos/effect/migration/annotations/effect__cli__Prompt.yaml index 8c1f6ffb1..04071fb7b 100644 --- a/repos/effect/migration/annotations/effect__cli__Prompt.yaml +++ b/repos/effect/migration/annotations/effect__cli__Prompt.yaml @@ -4,24 +4,42 @@ "@effect/cli/Prompt#All.Return": replacement: "Prompt.All.Return" note: "The collection result helper remains under Prompt.All." +"@effect/cli/Prompt#confirm": + replacement: "Prompt.Confirm" + note: "Use the renamed constructor." +"@effect/cli/Prompt#custom": + replacement: "Prompt.Custom" + note: "Use the renamed constructor; both overloads are preserved." "@effect/cli/Prompt#date": - replacement: "Prompt.date" + replacement: "Prompt.Date" note: "Use the moved constructor." "@effect/cli/Prompt#file": - replacement: "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" + replacement: "Prompt.Number" note: "Use the moved constructor; v4 also supports a default value." +"@effect/cli/Prompt#hidden": + replacement: "Prompt.Hidden" + note: "Use the renamed constructor." "@effect/cli/Prompt#integer": - replacement: "Prompt.integer" + replacement: "Prompt.Int" note: "Use the moved constructor; v4 also supports a default value." +"@effect/cli/Prompt#list": + replacement: "Prompt.List" + note: "Use the renamed constructor." "@effect/cli/Prompt#map": replacement: "Prompt.map" note: "Use the moved combinator." +"@effect/cli/Prompt#multiSelect": + replacement: "Prompt.MultiSelect" + note: "Use the renamed constructor." +"@effect/cli/Prompt#password": + replacement: "Prompt.Password" + note: "Use the renamed constructor." "@effect/cli/Prompt#Prompt": replacement: "Prompt.Prompt" note: "The model moved to effect/unstable/cli; quitting now fails with Terminal.QuitError." @@ -34,6 +52,18 @@ "@effect/cli/Prompt#PromptTypeId": replacement: "Prompt.isPrompt" note: "The public type-id symbol was removed; use the runtime guard." +"@effect/cli/Prompt#select": + replacement: "Prompt.Select" + note: "Use the renamed constructor." "@effect/cli/Prompt#text": - replacement: "Prompt.text" + replacement: "Prompt.String" note: "Use the moved constructor." +"@effect/cli/Prompt#toggle": + replacement: "Prompt.Toggle" + note: "Use the renamed constructor." +"@effect/cli/Prompt#Prompt.IntegerOptions": + replacement: "Prompt.IntOptions" + note: "Use the renamed public options type for Prompt.Int; option fields are preserved." +"@effect/cli/Prompt#Prompt.FloatOptions": + replacement: "Prompt.NumberOptions" + note: "Use the renamed public options type for Prompt.Number; it still extends the integer options type, now IntOptions." diff --git a/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml b/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml index 6edba3715..57ef342ba 100644 --- a/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml +++ b/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml @@ -1,9 +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." + note: "Moved into core Effect. Use v4 HttpApi identifiers and provide Sharding plus both Rpc.ServicesServer and Rpc.ServicesClient codec 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." + note: "Moved into core Effect; replace Rpc.Context with both Rpc.ServicesServer and Rpc.ServicesClient codec requirements, alongside Sharding." "@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/repos/effect/migration/annotations/effect__cluster__Reply.yaml b/repos/effect/migration/annotations/effect__cluster__Reply.yaml index aecd2243b..842dd729a 100644 --- a/repos/effect/migration/annotations/effect__cluster__Reply.yaml +++ b/repos/effect/migration/annotations/effect__cluster__Reply.yaml @@ -7,3 +7,6 @@ "@effect/cluster/Reply#TypeId": replacement: "none" note: "The reply marker is private in v4. Use Reply.isReply for runtime refinement." +"@effect/cluster/Reply#Reply": + replacement: "Reply.Reply(rpc, codecFor)" + note: "Pass the transport codec. Decoding replies requires Rpc.ServicesClient; encoding replies requires Rpc.ServicesServer." diff --git a/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml b/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml index ebbe457fe..32537e758 100644 --- a/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml +++ b/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml @@ -16,3 +16,6 @@ "@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. +"@effect/experimental/RateLimiter#RateLimiterStore": + replacement: RateLimiter.RateLimiterStore + note: "Use the Context.Service class from effect/unstable/persistence/RateLimiter. Custom tokenBucket implementations must return [remaining, elapsedMillis] instead of a number, preserving fractional counts and elapsed refill time." diff --git a/repos/effect/migration/annotations/effect__experimental__Reactivity.yaml b/repos/effect/migration/annotations/effect__experimental__Reactivity.yaml index 621a7f78d..d224797f0 100644 --- a/repos/effect/migration/annotations/effect__experimental__Reactivity.yaml +++ b/repos/effect/migration/annotations/effect__experimental__Reactivity.yaml @@ -6,10 +6,10 @@ 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. + note: Use the Reactivity interface for implementations and the Reactivity Context.Service value as the service key. Implementations require the TypeId brand; prefer Reactivity.make. 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. + replacement: effect/unstable/reactivity/Reactivity#Reactivity + note: "The named namespace member was removed; use the branded Reactivity interface directly. Prefer Reactivity.make, or include [Reactivity.TypeId]: Reactivity.TypeId in a custom implementation." "@effect/experimental/Reactivity#stream": replacement: effect/unstable/reactivity/Reactivity#stream note: Import stream from the v4 unstable Reactivity module. diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml b/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml index 4b343a35c..dee586b78 100644 --- a/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml +++ b/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml @@ -1,6 +1,6 @@ "@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." + note: "The named interface was inlined into the constructor options; chunkSize is now a number and the full options also contain evaluate and onError. The ignored bufferSize option was removed." "@effect/platform-bun/BunStream#FromWritableOptions": replacement: "Pick[0], \"endOnDone\" | \"encoding\">" note: "The named interface was inlined into BunSink.fromWritable and duplex constructor options." diff --git a/repos/effect/migration/annotations/effect__platform-node__index.yaml b/repos/effect/migration/annotations/effect__platform-node__index.yaml index d2e0c66e9..3e30ad165 100644 --- a/repos/effect/migration/annotations/effect__platform-node__index.yaml +++ b/repos/effect/migration/annotations/effect__platform-node__index.yaml @@ -1,3 +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." + note: "The explicit /index entrypoint was removed; import Node-prefixed namespaces from the @effect/platform-node package root or import specific modules directly. Undici is no longer in the root barrel; import it from @effect/platform-node/Undici or directly from undici." diff --git a/repos/effect/migration/annotations/effect__platform__Cookies.yaml b/repos/effect/migration/annotations/effect__platform__Cookies.yaml index 0cf3cfa23..ef4edc1e9 100644 --- a/repos/effect/migration/annotations/effect__platform__Cookies.yaml +++ b/repos/effect/migration/annotations/effect__platform__Cookies.yaml @@ -19,3 +19,6 @@ "@effect/platform/Cookies#unsafeSetAll": replacement: "Cookies.setAllUnsafe" note: "Renamed to put Unsafe last; the dual all-or-throw behavior is retained." +"@effect/platform/Cookies#CookiesError": + replacement: "Cookies.CookiesError" + note: "The error tag is CookiesError rather than CookieError. Update catchTag calls and _tag comparisons; validation details are in the reason field." diff --git a/repos/effect/migration/annotations/effect__platform__FileSystem.yaml b/repos/effect/migration/annotations/effect__platform__FileSystem.yaml index ddd7a88d3..a2b699595 100644 --- a/repos/effect/migration/annotations/effect__platform__FileSystem.yaml +++ b/repos/effect/migration/annotations/effect__platform__FileSystem.yaml @@ -13,9 +13,15 @@ "@effect/platform/FileSystem#FileTypeId": replacement: "typeof FileSystem.FileTypeId" note: "The runtime marker remains exported, but the separate type alias was removed." +"@effect/platform/FileSystem#GiB": + replacement: "ByteSize.gibibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#isFile": replacement: "FileSystem.isFile" note: "The guard remains after moving the module to effect/FileSystem." +"@effect/platform/FileSystem#KiB": + replacement: "ByteSize.kibibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#layerNoop": replacement: "FileSystem.layerNoop" note: "The helper remains after moving the module to effect/FileSystem." @@ -34,9 +40,15 @@ "@effect/platform/FileSystem#MakeTempFileOptions": replacement: "NonNullable[0]>" note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#MiB": + replacement: "ByteSize.mebibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#OpenFileOptions": replacement: "NonNullable[1]>" note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#PiB": + replacement: "ByteSize.pebibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#ReadDirectoryOptions": replacement: "NonNullable[1]>" note: "Operation option interfaces are inline in the v4 FileSystem service." @@ -47,11 +59,17 @@ 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." + replacement: "ByteSize.ByteSize" + note: "Use ByteSize.bytes or unit constructors for file sizes. Truncation lengths, buffer sizes, and read/write counts use number. File.seek takes and returns signed bigint positions; it can fail with PlatformError, including BadArgument when seeking before the start." +"@effect/platform/FileSystem#SizeInput": + replacement: "ByteSize.Input" + note: "File-size and path-backed range inputs use ByteSize.Input. String inputs must be non-negative decimal integers without leading zeros, followed by a canonical unit symbol or lowercase unit name, with at most one separating space. Parse external strings or fractional quantities with ByteSize.fromString (Option) or ByteSize.fromStringUnsafe (throws), then pass the resulting ByteSize. Truncation lengths, Web File ranges, and buffer sizes use number." "@effect/platform/FileSystem#StreamOptions": replacement: "NonNullable[1]>" - note: "Stream options are inline; bufferSize was removed while bytesToRead, chunkSize, and offset remain." + note: "Stream options are inline; bufferSize was removed, bytesToRead and offset accept ByteSize inputs, and chunkSize uses number." +"@effect/platform/FileSystem#TiB": + replacement: "ByteSize.tebibytes" + note: "Use the ByteSize binary unit constructor." "@effect/platform/FileSystem#WatchEventCreate": replacement: "FileSystem.WatchEvent.Create" note: "The constructor was removed; construct a tagged object with _tag: \"Create\" and path." diff --git a/repos/effect/migration/annotations/effect__platform__Headers.yaml b/repos/effect/migration/annotations/effect__platform__Headers.yaml index ef95fbf84..2cd2ffa57 100644 --- a/repos/effect/migration/annotations/effect__platform__Headers.yaml +++ b/repos/effect/migration/annotations/effect__platform__Headers.yaml @@ -29,11 +29,11 @@ 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." + replacement: "Schema.Headers" + note: "The encoded-record and self schemas were consolidated and moved to effect/Schema as Schema.Headers." "@effect/platform/Headers#schemaFromSelf": - replacement: "Headers.HeadersSchema" - note: "The encoded-record and self schemas were consolidated into HeadersSchema." + replacement: "Schema.Headers" + note: "The encoded-record and self schemas were consolidated and moved to effect/Schema as Schema.Headers." "@effect/platform/Headers#set": replacement: "Headers.set" note: "Retained with the same dual signature and lowercase key normalization." diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml b/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml index 3742d27e5..35231d587 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml @@ -8,8 +8,8 @@ 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." + replacement: "effect/unstable/httpapi/HttpApiBuilder#handler" + note: "The typed callback helper remains; names are now identifiers and API/group global error channels are gone. Pass the returned callback 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." @@ -25,6 +25,9 @@ "@effect/platform/HttpApiBuilder#HandlersTypeId": replacement: "none" note: "The exported symbol was removed; do not inspect or construct the private Handlers marker." +"@effect/platform/HttpApiBuilder#Middleware": + replacement: "none" + note: "The API-specific middleware service tag was removed. Declared HttpApiMiddleware services are applied while routes are built; use HttpRouter.middleware for additional global middleware." "@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." diff --git a/repos/effect/migration/annotations/effect__platform__HttpBody.yaml b/repos/effect/migration/annotations/effect__platform__HttpBody.yaml index 4e8e46f1b..8258bcc0c 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpBody.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpBody.yaml @@ -12,10 +12,10 @@ 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." + note: "Retained; bufferSize was replaced by numeric chunkSize. Offset and bytesToRead accept ByteSize.Input. Invalid ranges and a final EOF-clamped content length above Number.MAX_SAFE_INTEGER fail with PlatformError / BadArgument." "@effect/platform/HttpBody#fileInfo": replacement: "HttpBody.fileFromInfo" - note: "Renamed; it still uses supplied File.Info for content length and requires FileSystem." + note: "Renamed; it uses supplied File.Info with ByteSize size metadata and requires FileSystem. Offset and bytesToRead accept ByteSize.Input, while chunkSize is numeric. Invalid ranges and a final EOF-clamped content length above Number.MAX_SAFE_INTEGER fail with PlatformError / BadArgument." "@effect/platform/HttpBody#formData": replacement: "HttpBody.formData" note: "Retained with the same Web FormData input." diff --git a/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml b/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml index cc48c14c8..49154affc 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml @@ -25,3 +25,6 @@ "@effect/platform/HttpClientResponse#TypeId": replacement: "typeof HttpClientResponse.TypeId" note: "TypeId remains public but is now a string constant; use typeof in type position." +"@effect/platform/HttpClientResponse#HttpClientResponse": + replacement: "HttpClientResponse.HttpClientResponse" + note: "Custom implementations must supply the required url string. It represents the resolved URL including query parameters and excluding the hash, uses the final URL after redirects, and is empty when unknown." diff --git a/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml b/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml index f720fb5d1..055745ce5 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml @@ -1,9 +1,9 @@ "@effect/platform/HttpIncomingMessage#MaxBodySize": replacement: "HttpIncomingMessage.MaxBodySize" - note: "Changed from a Reference subclass holding Option to Context.Reference." + 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." + note: "The helper was removed; provide a ByteSize value or undefined directly." diff --git a/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml b/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml index ba4d67380..523d462f0 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml @@ -1,12 +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." + note: "The service is now a Context.Service class; use its Service member for the implementation type. Path-backed offset and bytesToRead accept ByteSize.Input, while chunkSize and all Web File range options use number." "@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." + note: "Retained; v4 returns the service implementation. The fileResponse callback receives contentLength as bigint, while start and end remain numbers. Web File range options use number." "@effect/platform/HttpPlatform#TypeId": replacement: "none" note: "The public type id was removed; use the HttpPlatform Context.Service class." diff --git a/repos/effect/migration/annotations/effect__platform__HttpServer.yaml b/repos/effect/migration/annotations/effect__platform__HttpServer.yaml index be98b6d5d..12076eaf5 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpServer.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpServer.yaml @@ -1,3 +1,6 @@ +"@effect/platform/HttpServer#Address": + replacement: "effect/unstable/net/NetAddress#SocketAddress" + note: "Replaced by the shared concrete internet-or-Unix socket address union." "@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." @@ -9,19 +12,22 @@ note: "Renamed; it provides the standard HTTP platform services." "@effect/platform/HttpServer#make": replacement: "HttpServer.make" - note: "Retained; it returns the Context.Service implementation." + note: "Retained; it returns the Context.Service implementation. Supply a NetAddress.SocketAddress for address, using NetAddress.inetAddressUnsafe for a parsed IP and port or NetAddress.unixPathAddress for a Unix path. Use NetAddress.inetAddress to validate the port through Result instead." +"@effect/platform/HttpServer#formatAddress": + replacement: "HttpServer.formatAddress" + note: "Accepts NetAddress.SocketAddress and delegates to NetAddress.formatUrlUnsafe. Scoped IPv6 addresses throw NetAddressError; Unix paths use the unix:// prefix." "@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." + replacement: "effect/unstable/net/NetAddress#InetAddress" + note: "Replaced by the shared resolved internet-address model; use address and port instead of hostname and port." "@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." + replacement: "effect/unstable/net/NetAddress#UnixPathAddress" + note: "Replaced by the shared Unix filesystem-path address model." "@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/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml b/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml index 86d7dfd14..f7b220253 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml +++ b/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml @@ -6,7 +6,7 @@ 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." + note: "Retained; offset and bytesToRead accept ByteSize.Input, while chunkSize uses number. Path-backed responses validate ranges and clamp content length to the available bytes." "@effect/platform/HttpServerResponse#formData": replacement: "HttpServerResponse.formData" note: "Moved unchanged." @@ -42,7 +42,7 @@ note: "Moved unchanged." "@effect/platform/HttpServerResponse#toWeb": replacement: "HttpServerResponse.toWeb" - note: "Retained, but the optional Runtime became an optional Context for stream execution." + note: "Retained, but the optional Runtime became an optional Context for stream execution. For a raw Web Response, outer headers override native headers and cookies append, including when the body is omitted. With withoutBody, the raw status and statusText are preserved unless the outer status is 204, 205, or 304, which takes precedence." "@effect/platform/HttpServerResponse#TypeId": replacement: "HttpServerResponse.isHttpServerResponse" note: "The response brand is private in v4; use the public runtime refinement." @@ -61,3 +61,6 @@ "@effect/platform/HttpServerResponse#urlParams": replacement: "HttpServerResponse.urlParams" note: "Retained and widened to accept UrlParams.Input." +"@effect/platform/HttpServerResponse#fileWeb": + replacement: "HttpServerResponse.fileWeb" + note: "Web File offset, bytesToRead, and chunkSize options use number, unlike path-backed ByteSize.Input ranges." diff --git a/repos/effect/migration/annotations/effect__platform__Multipart.yaml b/repos/effect/migration/annotations/effect__platform__Multipart.yaml index e5f43102e..3e1a86345 100644 --- a/repos/effect/migration/annotations/effect__platform__Multipart.yaml +++ b/repos/effect/migration/annotations/effect__platform__Multipart.yaml @@ -18,10 +18,10 @@ 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." + note: "Now a Context.Reference; provide a value such as ByteSize.bytes(100)." "@effect/platform/Multipart#MaxFileSize": replacement: "Multipart.MaxFileSize" - note: "The setting remains as a Context.Reference; use undefined rather than Option.none for no limit." + note: "Now a Context.Reference; provide ByteSize.bytes(100), for example, or undefined 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." @@ -42,10 +42,10 @@ 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." + note: "Provide a ByteSize value, such as ByteSize.bytes(100). To normalize ByteSize.Input options, use Multipart.limitsServices." "@effect/platform/Multipart#withMaxFileSize": replacement: "Effect.provideService(Multipart.MaxFileSize, size)" - note: "Provide the v4 Context.Reference around the effect, converting Option.none to undefined." + note: "Replace Option.none with undefined and Option.some(value) with a normalized ByteSize value. To normalize ByteSize.Input options, use Multipart.limitsServices." "@effect/platform/Multipart#withMaxParts": replacement: "Effect.provideService(Multipart.MaxParts, count)" note: "Provide the v4 Context.Reference around the effect, converting Option.none to undefined." @@ -54,4 +54,4 @@ 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." + note: "Limit fields now use optional plain numbers or ByteSize inputs; convert Option.none to undefined and Option.some(value) to value." diff --git a/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml b/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml index 683b937f3..0bb6527bc 100644 --- a/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml +++ b/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml @@ -10,6 +10,9 @@ "@effect/platform/OpenApiJsonSchema#Array": replacement: "effect/JsonSchema#JsonSchema" note: "The narrow node interfaces were consolidated into the general object model." +"@effect/platform/OpenApiJsonSchema#Boolean": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow boolean interface was consolidated into the open, dialect-neutral JSON Schema object model." "@effect/platform/OpenApiJsonSchema#Empty": replacement: "effect/JsonSchema#JsonSchema" note: "The narrow node interfaces and special id shapes were removed." @@ -31,6 +34,12 @@ "@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#Never": + replacement: "effect/JsonSchema#JsonSchema" + note: "The special never-schema interface was consolidated into the open JSON Schema object model; represent it with a not constraint." +"@effect/platform/OpenApiJsonSchema#Number": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow number interface was consolidated into the open, dialect-neutral JSON Schema object model." "@effect/platform/OpenApiJsonSchema#Numeric": replacement: "effect/JsonSchema#JsonSchema" note: "The narrow numeric interfaces were consolidated into the general object model." @@ -43,3 +52,9 @@ "@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." +"@effect/platform/OpenApiJsonSchema#String": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow string interface was consolidated into the open, dialect-neutral JSON Schema object model." +"@effect/platform/OpenApiJsonSchema#Void": + replacement: "effect/JsonSchema#JsonSchema" + note: "The special void-schema interface was consolidated into the open JSON Schema object model." diff --git a/repos/effect/migration/annotations/effect__platform__SocketServer.yaml b/repos/effect/migration/annotations/effect__platform__SocketServer.yaml index b7a20dbd8..6367ff8e1 100644 --- a/repos/effect/migration/annotations/effect__platform__SocketServer.yaml +++ b/repos/effect/migration/annotations/effect__platform__SocketServer.yaml @@ -1,9 +1,12 @@ +"@effect/platform/SocketServer#Address": + replacement: "effect/unstable/net/NetAddress#SocketAddress" + note: "Replaced by the shared concrete internet-or-Unix socket address union." "@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." + replacement: "effect/unstable/net/NetAddress#InetAddress" + note: "Replaced by the shared resolved internet-address model; use address and port instead of hostname and port." "@effect/platform/SocketServer#UnixAddress": - replacement: "SocketServer.UnixAddress" - note: "The API moved to effect/unstable/socket/SocketServer and retains this name." + replacement: "effect/unstable/net/NetAddress#UnixPathAddress" + note: "Replaced by the shared Unix filesystem-path address model." diff --git a/repos/effect/migration/annotations/effect__platform__Url.yaml b/repos/effect/migration/annotations/effect__platform__Url.yaml index 951df8c57..de6c44677 100644 --- a/repos/effect/migration/annotations/effect__platform__Url.yaml +++ b/repos/effect/migration/annotations/effect__platform__Url.yaml @@ -1,3 +1,6 @@ +"@effect/platform/Url#fromString": + replacement: "Url.fromString" + note: "Retained; returns Result with IllegalArgumentError instead of Either with IllegalArgumentException." "@effect/platform/Url#setUrlParams": replacement: "Url.setUrlParams" note: "Retained and widened to accept UrlParams.Input." diff --git a/repos/effect/migration/annotations/effect__platform__UrlParams.yaml b/repos/effect/migration/annotations/effect__platform__UrlParams.yaml index a799f823a..2e4a856df 100644 --- a/repos/effect/migration/annotations/effect__platform__UrlParams.yaml +++ b/repos/effect/migration/annotations/effect__platform__UrlParams.yaml @@ -23,23 +23,23 @@ 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." + replacement: "Schema.UrlParams" + note: "The declaration schema for the v4 wrapper moved to effect/Schema." "@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) }))" + replacement: "Schema.String.pipe(Schema.decodeTo(Schema.UrlParams, { 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." + replacement: "Schema.JsonFromUrlParamsField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect)" + note: "The field codec moved to effect/Schema. Compose it with the target schema, then decode it." "@effect/platform/UrlParams#schemaParse": - replacement: "UrlParamsFromString.pipe(Schema.decodeTo(UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))))" + replacement: "UrlParamsFromString.pipe(Schema.decodeTo(Schema.RecordFromUrlParams.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." + replacement: "Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema))" + note: "RecordFromUrlParams is a base codec in effect/Schema; 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." + replacement: "Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema), Schema.decodeEffect)" + note: "Compose the record codec from effect/Schema with the target schema and decode it." "@effect/platform/UrlParams#set": replacement: "UrlParams.set" note: "Retained and replaces all existing values for the key." @@ -49,3 +49,6 @@ "@effect/platform/UrlParams#toString": replacement: "UrlParams.toString" note: "Retained and broadened to accept any UrlParams.Input." +"@effect/platform/UrlParams#UrlParams": + replacement: "UrlParams.UrlParams" + note: "Import UrlParams from effect/unstable/http. It is now a branded iterable object with a params field rather than a ReadonlyArray; construct it with UrlParams.make or UrlParams.fromInput." diff --git a/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml b/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml index d9ae6977b..8c497bbb2 100644 --- a/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml +++ b/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml @@ -6,10 +6,11 @@ note: "Wrapping an existing node-pg Pool was removed. Provide connection settings to PgClient.layer instead." "@effect/sql-pg/PgClient#PgClient": replacement: "@effect/sql-pg/PgClient#PgClient" - note: "Retained; the service value is now a Context.Service." + note: "Retained; the service value is now a Context.Service. The listen method returns a scoped Effect acquiring a Queue.Dequeue; consume it with Queue operations or Stream.fromQueue and read each notification's payload. Connection failures after registration fail the queue with the original SqlError; scope closure interrupts consumers." "@effect/sql-pg/PgClient#PgClientConfig": + include: true 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." + note: "Use PgClientConfig for base settings and PgPoolConfig for make/layer; pool sizing, idle timeout, and connection TTL moved to PgPoolConfig. The types option takes a PgTypes.Registry rather than node-pg custom types. Timestamp and timestamptz results (including array elements) are Date values with millisecond precision; infinity, -infinity, and out-of-range values become invalid Dates. Numeric readers can use getTime() or register numeric codecs. Date parameters bind as timestamptz; use PgTypes.timestamp(value) to preserve UTC fields when writing a timestamp column instead of applying the session TimeZone." "@effect/sql-pg/PgClient#PgClientFromPoolOptions": replacement: none note: "The node-pg Pool wrapper options were removed with fromPool. Use PgClient.PgPoolConfig with PgClient.make or PgClient.layer." diff --git a/repos/effect/migration/annotations/effect__sql__Model.yaml b/repos/effect/migration/annotations/effect__sql__Model.yaml index 00db99efc..6c82eda85 100644 --- a/repos/effect/migration/annotations/effect__sql__Model.yaml +++ b/repos/effect/migration/annotations/effect__sql__Model.yaml @@ -10,6 +10,9 @@ "@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#Date": + replacement: "effect/unstable/schema/Model#Date" + note: "Moved; still serializes DateTime.Utc as a YYYY-MM-DD string." "@effect/sql/Model#DateTimeFromDate": replacement: "effect/Schema#DateTimeUtcFromDate" note: "Moved to core Schema and retains Date to DateTime.Utc conversion." @@ -24,7 +27,7 @@ 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." + note: "Returns RequestResolvers instead of callable loaders; execute with SqlResolver.request and use RequestResolver delay/batch combinators for batching controls. The insert resolver requires model decoding services as well as insert-schema encoding services." "@effect/sql/Model#Override": replacement: "effect/unstable/schema/Model#Override" note: "Moved with the same explicit-default override purpose." diff --git a/repos/effect/migration/annotations/effect__vitest__index.yaml b/repos/effect/migration/annotations/effect__vitest__index.yaml index a40e32ca2..515c9201a 100644 --- a/repos/effect/migration/annotations/effect__vitest__index.yaml +++ b/repos/effect/migration/annotations/effect__vitest__index.yaml @@ -18,88 +18,88 @@ 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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." + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape." "@effect/vitest/index#CollectLineNumbers": replacement: "vitest/node#TypeCheckCollectLineNumbers" note: "Vitest 3 deprecated the root alias in favor of this renamed vitest/node type." @@ -147,25 +147,25 @@ 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." + note: "The separate context alias was removed. Vitest 5 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." + note: "The separate context alias was removed. Vitest 5 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." + replacement: "vitest/runtime#Environment" + note: "This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime." "@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." + replacement: "vitest/runtime#EnvironmentReturn" + note: "This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime." "@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." + replacement: "vitest/runtime#VmEnvironmentReturn" + note: "This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime." "@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." + note: "Vitest 5 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." + note: "Vitest 5 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." @@ -191,38 +191,38 @@ 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." + replacement: "vitest/node#Reporter" + note: "Import Reporter from vitest/node; the deprecated vitest/reporters entrypoint was removed in Vitest 5." "@effect/vitest/index#UserConfig": replacement: "vitest/config#TestUserConfig" - note: "Vitest 4 exposes its config as TestUserConfig; ViteUserConfig is the separate Vite configuration type." + note: "Vitest 5 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." + note: "The v3 built-in poolOptions object was removed. Move its fields to Vitest 5 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." + replacement: "none" + note: "Vitest 5 deprecates @vitest/runner and does not expose this internal state type. Extend TestRunner from vitest and use its public methods instead." "@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." + replacement: "ReturnType" + note: "Derive the hook collection from Vitest 5's public TestRunner API; 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." + note: "No named Vitest 5 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." + note: "Infer the callback from the public hook function, or derive it with Parameters[0] and the corresponding hook name." "@effect/vitest/index#ModuleCache": replacement: "none" - note: "Vitest 3 marked this unused internal cache shape deprecated; Vitest 4 has no public replacement." + note: "Vitest 3 marked this unused internal cache shape deprecated; Vitest 5 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." + note: "Vitest 3 marked this type unsupported. Use Environment from vitest/runtime for custom environments." "@effect/vitest/index#ResolveIdFunction": replacement: "none" note: "This deprecated vite-node callback was removed. Use Vite environment or module-runner APIs." @@ -232,3 +232,48 @@ "@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." +"@effect/vitest/index#bench": + replacement: "vitest#test" + note: "Vitest 5 removes the top-level bench export. Destructure bench from a regular test's context and await bench(name, fn).run(); use skip, only, or todo on the enclosing test." +"@effect/vitest/index#BenchFactory": + replacement: "vitest#Bench" + note: "Use the Vitest 5 test-context bench fixture type. It is no longer the tinybench factory constructor." +"@effect/vitest/index#BenchFunction": + replacement: "vitest#BenchFn" + note: "Use BenchFn for the callback passed to the Vitest 5 test-context bench fixture." +"@effect/vitest/index#BenchTask": + replacement: "vitest#BenchRegistration" + note: "Migrate to a fixture registration and await its run() method; review its fields instead of treating it as a tinybench task." +"@effect/vitest/index#BenchTaskResult": + replacement: "vitest#BenchResult" + note: "Use the result returned by awaiting the Vitest 5 fixture registration's run() method." +"@effect/vitest/index#Benchmark": + replacement: "vitest#TestBenchmark" + note: "Use TestBenchmark for recorded benchmark data on a test; benchmarks are no longer standalone test tasks." +"@effect/vitest/index#BenchmarkAPI": + replacement: "vitest#Bench" + note: "Use the test-context bench fixture. Move skip, only, and todo to the enclosing test." +"@effect/vitest/index#BenchmarkResult": + replacement: "vitest#BenchResult" + note: "Use the result returned by awaiting the Vitest 5 fixture registration's run() method; review its changed fields." +"@effect/vitest/index#BenchmarkRunner": + replacement: "vitest#BenchmarkProvider" + note: "Vitest 5 runs benchmarks through providers. Export a BenchmarkProvider with an asynchronous run(group) method for a custom engine." +"@effect/vitest/index#ExpectPollOptions": + replacement: "NonNullable[1]>" + note: "Vitest 5 removes the named options export; derive the options from the public expect.poll function." +"@effect/vitest/index#Assertion": + replacement: "vitest#Assertion" + note: "Vitest 5 takes the matcher return type first. Replace Assertion with Assertion or Assertion, T> for asynchronous assertions." +"@effect/vitest/index#Matchers": + replacement: "vitest#Matchers" + note: "Augment vitest.Matchers for custom matchers. R is the matcher return type and T is the received value; @vitest/expect no longer shares Vitest's assertion state." +"@effect/vitest/index#it": + replacement: "@effect/vitest#it" + note: "Effect helpers retain their calling convention. Vitest 5 removes it.sequential; pass { concurrent: false } to the native test or as the Effect helper's third argument." +"@effect/vitest/index#test": + replacement: "vitest#test" + note: "Vitest 5 removes test.sequential and sequential options. Pass { concurrent: false } to opt out of inherited concurrency." +"@effect/vitest/index#describe": + replacement: "vitest#describe" + note: "Vitest 5 removes describe.sequential and sequential options. Use describe(name, { concurrent: false }, body) for suites that depend on ordering." diff --git a/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml b/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml index f30fc24b4..6e01c63c9 100644 --- a/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml +++ b/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml @@ -15,7 +15,7 @@ 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." + note: "Moved into core Effect with the same exit recording and suspension propagation behavior. Provide both decoding and encoding services for the success and error schemas; recording the exit requires encoding services." "@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." diff --git a/repos/effect/migration/schema.md b/repos/effect/migration/schema.md index bc5ead367..e9d71ad07 100644 --- a/repos/effect/migration/schema.md +++ b/repos/effect/migration/schema.md @@ -59,7 +59,7 @@ This document maps v3 Schema APIs to their v4 equivalents. Simple renames and ar | `required(schema)` | `schema.mapFields(Struct.map(Schema.requiredKey))` | restructure | | `extend(structB)` | `mapFields(Struct.assign(fieldsB))` or `fieldsAssign(fieldsB)` | restructure | | `transform(from, to, { decode, encode })` | `from.pipe(decodeTo(to, SchemaTransformation.transform({ decode, encode })))` | restructure | -| `transformOrFail(from, to, { decode, encode })` | `from.pipe(decodeTo(to, { decode: SchemaGetter.transformOrFail(...), ... }))` | restructure | +| `transformOrFail(from, to, { decode, encode })` | `from.pipe(decodeTo(to, { decode: SchemaGetter.transformEffect(...), ... }))` | restructure | | `transformLiteral(from, to)` | `Literal(from).transform(to)` | restructure | | `transformLiterals([0,"a"], [1,"b"])` | `Literals([0, 1]).transform(["a", "b"])` | restructure | | `attachPropertySignature("k", "v")` | `mapFields(f => ({...f, k: tagDefaultOmit("v")}))` | restructure | @@ -121,7 +121,7 @@ Note: `positive`, `negative`, `nonNegative`, `nonPositive` have been removed in ### Utility renames -`equivalence` → `toEquivalence`, `arbitrary` → `toArbitrary`, `pretty` → `toFormatter`, `standardSchemaV1` → `toStandardSchemaV1` +`equivalence` → `toEquivalence`, `arbitrary` → [`Arbitrary.schema`](#migrating-from-the-fast-check-bridge-to-native-arbitrary) from `effect/unstable/arbitrary`, `pretty` → `toFormatter`, `standardSchemaV1` → `toStandardSchemaV1` ## Detailed migrations @@ -901,7 +901,7 @@ import { Effect, Number, Schema, SchemaGetter, SchemaIssue } from "effect" const NumberFromString = Schema.String.pipe( Schema.decodeTo(Schema.Number, { - decode: SchemaGetter.transformOrFail((s) => { + decode: SchemaGetter.transformEffect((s) => { const n = Number.parse(s) if (n === undefined) { return Effect.fail(new SchemaIssue.InvalidValue()) @@ -1093,3 +1093,345 @@ function split(separator: string) { ) } ``` + +## Migrating from the fast-check bridge to native Arbitrary + +This section covers migration from the fast-check bridge published in `effect@4.0.0-rc.109` to the native, +Schema-first module at `effect/unstable/arbitrary`. + +The new module removes fast-check from the `effect` package. Applications may still install and use fast-check +directly, but Effect Schema generation and `@effect/vitest` property tests no longer depend on it. + +For the new API and its semantics, see [Arbitrary in Effect](../packages/effect/ARBITRARY.md). + +### Import changes + +The following APIs have been removed: + +- `effect/testing/FastCheck`; +- `Schema.toArbitrary`; +- `Schema.Arbitrary`; +- the legacy `Schema.Annotations.ToArbitrary` contract and declaration-level `toArbitrary` annotation; +- the legacy `arbitrary` filter annotation; +- raw fast-check arbitrary inputs and `fastCheck` options in `@effect/vitest`. + +Import the native module explicitly: + +```ts +import { Arbitrary } from "effect/unstable/arbitrary" +``` + +If other tests still use fast-check-specific APIs, add fast-check as a direct development dependency and import it +from `"fast-check"`. Do not import it through Effect. + +### Generating samples + +Previously, `Schema.toArbitrary` returned a factory that needed the fast-check module: + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +const Person = Schema.Struct({ + name: Schema.String, + age: Schema.Int +}) + +const personArbitrary = Schema.toArbitrary(Person)(FastCheck) +const samples = FastCheck.sample(personArbitrary, { numRuns: 20, seed: 42 }) +``` + +Now derive and sample through the Effect-native module: + +```ts +import { Effect, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const Person = Schema.Struct({ + name: Schema.String, + age: Schema.Int +}) + +const personArbitrary = Arbitrary.schema(Person) +const samples = await Effect.runPromise( + Arbitrary.sampleEffect(personArbitrary, { count: 20, seed: 42 }) +) +``` + +`Arbitrary.sampleEffect` returns an `Effect` because sampling is interruptible, uses Effect `Random` when no seed is +provided, and reports bounded generation exhaustion as a typed `SampleError`. + +The generated values still use the decoded Schema `Type`. The sequence and distribution are not compatible with +fast-check, even when the same numeric seed is used. + +### Checking properties + +Previously, fast-check owned both the property and the runner: + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +const integer = Schema.toArbitrary(Schema.Int)(FastCheck) + +FastCheck.assert( + FastCheck.property(integer, (value) => Number.isInteger(value)), + { numRuns: 100, seed: 42 } +) +``` + +Now `Arbitrary.checkEffect` runs a pure or Effectful property and returns a structured result: + +```ts +import { Effect, Schema } from "effect" +import { Arbitrary } from "effect/unstable/arbitrary" + +const result = await Effect.runPromise( + Arbitrary.checkEffect( + Arbitrary.schema(Schema.Int), + (value) => Number.isInteger(value), + { runs: 100, seed: 42 } + ) +) +``` + +Unlike `FastCheck.assert`, `Arbitrary.checkEffect` does not throw for an ordinary falsification. Handle `Passed`, +`Falsified`, `Exhausted`, and `ReplayMismatch` explicitly, or use `@effect/vitest`, which converts non-passing results +into test failures. + +Typed failures from Effectful properties are preserved in `Falsified.failure`. Defects and interruption continue +through the returned Effect. + +### Option mapping + +The most common options map as follows: + +| Previous fast-check option | Native option | Migration note | +| -------------------------- | -------------------- | ------------------------------------------------------------------ | +| `numRuns` | `count` or `runs` | Use `count` for `sampleEffect` and `runs` for `checkEffect`. | +| `seed` | `seed` | The type is compatible, but generated sequences are not. | +| `path` | `replay` | Existing fast-check paths cannot be converted. | +| `maxSkipsPerRun` | `maxDiscards` | Native uses one absolute discard budget, not a multiplier per run. | +| `examples` | No direct equivalent | Keep explicit regression cases as ordinary tests. | +| `endOnFailure` | `maxShrinks` | Use `maxShrinks: 0` to stop at the initial failure. | +| `interruptAfterTimeLimit` | Effect interruption | Apply an Effect or test timeout around the check. | +| `skipAllAfterTimeLimit` | No direct equivalent | Prefer explicit run and discard bounds. | +| `verbose` | No direct equivalent | Inspect `CheckResult` or use `@effect/vitest` failure output. | + +Review any less common fast-check runner option manually. The native API deliberately does not reproduce the complete +`fc.Parameters` surface. + +### Replay migration + +Fast-check replay used a seed plus a shrink `path`. Native replay uses one opaque token returned by a `Falsified` +result: + +```ts +const replayed = Arbitrary.checkEffect(arbitrary, property, { + replay: previousFailure.replay +}) +``` + +There is no conversion from a fast-check seed and path to a native replay token. Re-run the property with the native +engine, then record the new token from its `Falsified` result. + +Replay tokens are intended for reproducing and diagnosing a current failure. Because the module is unstable, they are +not guaranteed to survive upgrades. Preserve important failing inputs as explicit regression tests. + +Array shrinking now tries deleting prefixes and interior blocks as well as suffixes. Products and objects retain child +shrink candidates when exploring other branches. Shrunk outputs and replay paths can therefore differ from earlier +native releases, including RC.115. Re-run affected properties to record new tokens. +For arrays of composed Arbitraries, use `Arbitrary.array(item, { maxLength: 50 })` instead of shrinking a generated +length through `flatMap` and `all`; deletions then preserve the remaining generated elements. + +### Migrating declaration annotations + +The old `toArbitrary` annotation directly constructed a fast-check arbitrary and exposed fast-check recursion and +constraint details: + +```ts +import { Schema } from "effect" + +class UserId { + readonly value: number + constructor(value: number) { + this.value = value + } +} + +const UserIdSchema = Schema.instanceOf(UserId, { + toArbitrary: () => (fc) => fc.integer({ min: 1, max: 1_000_000 }).map((value) => new UserId(value)) +}) +``` + +The native `toCodecArbitrary` annotation describes a generatable representation as a Schema `Link`: + +```ts +import { Schema, SchemaTransformation } from "effect" + +class UserId { + readonly value: number + constructor(value: number) { + this.value = value + } +} + +const UserIdSchema = Schema.instanceOf(UserId, { + toCodecArbitrary: () => + Schema.link()( + Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000_000 })), + SchemaTransformation.transform({ + decode: (value) => new UserId(value), + encode: (id) => id.value + }) + ) +}) +``` + +Before adding `toCodecArbitrary`, check whether the declaration already has a useful `toCodecJson` or `toCodec`. +Native derivation falls back to those canonical codecs automatically. Add an arbitrary-specific Link only when the +canonical representation is opaque or generates valid values too rarely. + +The migration changes where generation logic lives: + +| Legacy contract | Native contract | +| ---------------------------------------------- | -------------------------------------------------------------- | +| Returns a `fast-check.Arbitrary` | Returns a `SchemaAST.Link` through `Schema.link`. | +| Receives generated arbitrary type parameters | Receives decoded Schema type parameters. | +| Receives fast-check constraints and recursion | Receives normalized constraints. | +| Manages terminal recursive branches explicitly | Leaves recursion analysis and budgets to the native compiler. | +| Uses arbitrary combinators | Uses Schema constructors, checks, and a Schema transformation. | + +The original declaration remains authoritative. Values decoded by the Link are checked against it. Failed decodes and +rejected values become bounded discards. + +#### Custom filter metadata + +The old `arbitrary` filter annotation has been replaced by `arbitraryConstraint`. Ordinary custom filters continue to +work as residual filters without generation metadata: + +```ts +import { Schema } from "effect" + +const Even = Schema.Int.check( + Schema.makeFilter((value) => value % 2 === 0) +) +``` + +Residual filtering is bounded, so a very selective or impossible predicate may produce `SampleError` or `Exhausted`. + +If the previous annotation supplied a recognized constructive constraint, move it to `arbitraryConstraint` and adapt +its shape. The predicate remains authoritative: + +```ts +import { Order, Schema } from "effect" + +const Positive = Schema.Number.check( + Schema.makeFilter( + (value) => value > 0, + { + arbitraryConstraint: { + order: Order.Number, + minimum: 0, + exclusiveMinimum: true + } + } + ) +) +``` + +The main constraint-shape changes are: + +| Previous field | Native field | +| ------------------------------------ | ------------------------------------------------------------------- | +| `ordered.order` | `order` | +| `ordered.minimum` / `maximum` | `minimum` / `maximum` | +| `ordered.exclusiveMinimum` / maximum | `exclusiveMinimum: true` / `exclusiveMaximum: true` | +| `integer: true` | `number: "integer"` | +| `noNaN` and `noInfinity` | `number: "finite"` when both restrictions apply | +| collection `minLength` / `maxLength` | `minLength`, `minSize`, or `minProperties` and its matching maximum | +| string pattern | `{ source, flags }` in `patterns` | +| `unique: true` | `uniqueBy: identity` | +| `candidate` | No direct equivalent | + +Choose the cardinality field that matches the Schema domain: `minLength` and `maxLength` for strings and arrays, +`minSize` and `maxSize` for sized collections, and `minProperties` and `maxProperties` for object properties. + +For an opaque declaration that needs a reusable statistically better source domain, express that source as a Schema +Link with `toCodecArbitrary`. + +### Migrating `@effect/vitest` + +Property inputs may be Schemas, native Arbitraries, or mixtures of both. + +Schema-only properties need only an option rename: + +```ts +// Before +it.prop( + "commutative", + [Schema.Int, Schema.Int], + ([a, b]) => a + b === b + a, + { fastCheck: { numRuns: 200, seed: 42 } } +) + +// After +it.prop( + "commutative", + [Schema.Int, Schema.Int], + ([a, b]) => a + b === b + a, + { arbitrary: { runs: 200, seed: 42 } } +) +``` + +Raw or mixed fast-check inputs are no longer accepted: + +```ts +// No longer supported +it.prop("raw arbitrary", [fc.integer()], ([value]) => Number.isInteger(value)) +it.prop("mixed", [Schema.String, fc.integer()], ([text, value]) => true) +``` + +Replace those inputs with Schemas when they describe a domain supported by Schema, or compose a native Arbitrary: + +```ts +import { Arbitrary } from "effect/unstable/arbitrary" + +const integer = Arbitrary.schema(Schema.Int) + +it.prop("native arbitrary", [integer], ([value]) => Number.isInteger(value)) +it.prop("mixed", [Schema.String, integer], ([text, value]) => typeof text === "string" && Number.isInteger(value)) +``` + +If a test genuinely needs a fast-check-specific arbitrary or runner feature, use fast-check directly with Vitest +rather than passing it through `@effect/vitest`. + +`it.prop`, `it.effect.prop`, and `it.live.prop` all accept native check options under `arbitrary`. + +### Behavioral differences to review + +Migration is not only an import rename. Review the following differences: + +- native generation and shrinking have different distributions and may find different shrunk inputs; +- native checking returns structured results instead of using fast-check's assertion exceptions; +- generation that cannot find enough valid samples is bounded and reports `SampleError` or `Exhausted`; +- pure and Effectful properties share one interruptible runner; +- recursive and mutually recursive Schemas are analyzed as a graph and must have a finite generation path; +- replay tokens, seeds, and shrink paths are not compatible with fast-check; +- generated values are the decoded Schema `Type`; +- properties must not mutate generated values. + +### Migration checklist + +1. Replace `effect/testing/FastCheck` imports. Use the native Arbitrary module for Schema generation and import + `"fast-check"` directly only where it is still independently required. +2. Replace `Schema.toArbitrary(schema)(FastCheck)` with `Arbitrary.schema(schema)`. +3. Replace `FastCheck.sample` with `Arbitrary.sampleEffect` and run the returned Effect. +4. Replace `FastCheck.check` or `FastCheck.assert` for Schema-derived inputs with `Arbitrary.checkEffect`, then handle its + structured result. +5. Rename `@effect/vitest` options from `fastCheck` to `arbitrary` and convert `numRuns` to `runs`. +6. Replace raw fast-check inputs in `@effect/vitest` with Schemas or native Arbitraries. +7. Migrate declaration-level `toArbitrary` callbacks to the `toCodecArbitrary` Link-returning contract and replace old + filter-level `arbitrary` annotations with `arbitraryConstraint`. +8. Re-run properties with the native engine and record new replay tokens or explicit regression examples. +9. Review discard limits for selective custom filters. diff --git a/repos/effect/migration/v3-to-v4.md b/repos/effect/migration/v3-to-v4.md index 00b867a78..e178782b4 100644 --- a/repos/effect/migration/v3-to-v4.md +++ b/repos/effect/migration/v3-to-v4.md @@ -2,9 +2,9 @@ # v3 to v4 Migration Reference -Base: `origin/v3` (`7c6e1e5d2ac9dfe00649a65fa80a61dcc14d55ae`) +Base: `origin/v3` (`1ce1e62367e67a04e63bdf62ce2911cfee8c716c`) -Head: `origin/main` (`53843f6490f4eebaf1eeb91fdcc5f1c542b0e132`) +Head: `origin/main` (`3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5`) This file is generated from the API diff and `migration/annotations/*.yaml`. @@ -506,7 +506,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `@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-node/index` -> `@effect/platform-node`: The explicit /index entrypoint was removed; import Node-prefixed namespaces from the @effect/platform-node package root or import specific modules directly. Undici is no longer in the root barrel; import it from @effect/platform-node/Undici or directly from undici. - `@effect/platform/ChannelSchema` -> `effect/ChannelSchema` - `@effect/platform/Command` -> `effect/unstable/process/ChildProcess` - `@effect/platform/CommandExecutor` -> `effect/unstable/process/ChildProcessSpawner` @@ -4164,6 +4164,12 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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-openai/OpenAiTool` + +- `OpenAiTool.WebSearch` -> `OpenAiTool.WebSearch`: Results now include action alongside status. Only completed is successful; other statuses produce failure results. Inspect isFailure before treating a result as a successful search. Inspect each search source by its type: URL sources have url, while API sources have name and no url. + +- `OpenAiTool.WebSearchPreview` -> `OpenAiTool.WebSearchPreview`: Results now include action alongside status. Only completed is successful; other statuses produce failure results. Inspect isFailure before treating a result as a successful search. Inspect each search source by its type: URL sources have url, while API sources have name and no url. + ### `@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. @@ -4878,7 +4884,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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. +- `OpenRouterClient.Service` -> `OpenRouterClient.Service`: Still exported in v4; adapt to the regenerated client, revised request and response schemas, and the new streaming result tuple. Custom service implementations and mocks must also provide createDecisions, returning an Effect of [decoded DecisionsResponse, HttpClientResponse] with AiError failures. ### `@effect/ai-openrouter/OpenRouterConfig` @@ -4898,6 +4904,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.HttpRequestDetails` -> `AiError.HttpRequestDetails`: Retained in effect/unstable/ai/AiError and also re-exported as Response.HttpRequestDetails. Hash is an optional string instead of Option, headers may contain Redacted strings, and method includes TRACE. + - `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. @@ -4910,10 +4918,20 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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/Chat` + +- `Chat.Chat` -> `Chat.Chat`: Use the Chat interface for implementations and the Context.Service value as the service key; it is no longer a class. Chat.empty and Chat.fromPrompt construct branded chat sessions. + +- `Chat.Service` -> `Chat.Chat`: Service was renamed to Chat. Implementations require [Chat.TypeId]: Chat.TypeId; use Chat.empty or Chat.fromPrompt to create a session. + ### `@effect/ai/EmbeddingModel` +- `EmbeddingModel.EmbeddingModel` -> `EmbeddingModel.EmbeddingModel`: Use the EmbeddingModel interface for implementations and the Context.Service value as the service key; it is no longer a class. Prefer EmbeddingModel.make to construct the branded implementation. + - `EmbeddingModel.Result`: TODO: needs guidance +- `EmbeddingModel.Service` -> `EmbeddingModel.EmbeddingModel`: Service was renamed to EmbeddingModel. Implementations require [EmbeddingModel.TypeId]: EmbeddingModel.TypeId; EmbeddingModel.make supplies the brand. + - `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` @@ -4926,6 +4944,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `LanguageModel.ExtractContext` -> `LanguageModel.ExtractServices`: Renamed in effect/unstable/ai/LanguageModel. ExtractServices infers toolkit handler, result-decoding, and effectful-toolkit service requirements. +- `LanguageModel.LanguageModel` -> `LanguageModel.LanguageModel`: Use the LanguageModel interface for implementations and the Context.Service value as the service key; it is no longer a class. Prefer LanguageModel.make to construct the branded implementation. + +- `LanguageModel.Service` -> `LanguageModel.LanguageModel`: Service was renamed to LanguageModel. Implementations require [LanguageModel.TypeId]: LanguageModel.TypeId; LanguageModel.make supplies the brand. + ### `@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. Binary image, audio, and blob data still use Uint8Array values with base64 wire encoding. @@ -4940,6 +4962,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/ai/McpServer` +- `McpServer.McpServer` -> `McpServer.McpServer`: The service is now a Context.Service. initializedClients and notificationsMailbox are no longer exposed. Registration callbacks use McpSchema.McpRequestContext; McpServerClient is only supplied for initialized stateful requests. Prefer registerToolkit, registerResource, and registerPrompt over implementing the registry shape directly. + - `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. @@ -4948,8 +4972,20 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.prompt` -> `McpServer.prompt`: Moved to effect/unstable/ai/McpServer. The registration layer excludes McpSchema.McpRequestContext from prompt decoding and handler requirements instead of McpServerClient. + +- `McpServer.registerPrompt` -> `McpServer.registerPrompt`: Moved to effect/unstable/ai/McpServer. Prompt decoding and handler requirements now exclude McpSchema.McpRequestContext instead of McpServerClient; use the request context for protocol-neutral client metadata. + +- `McpServer.registerResource` -> `McpServer.registerResource`: Moved to effect/unstable/ai/McpServer. Resource and completion handler requirements now exclude McpSchema.McpRequestContext instead of McpServerClient; use the request context for protocol-neutral client metadata. + +- `McpServer.registerToolkit` -> `McpServer.registerToolkit`: Moved to effect/unstable/ai/McpServer. Handler requirements now exclude McpSchema.McpRequestContext instead of McpServerClient. Strict tools reject excess input properties; raw JSON Schema dynamic tools cannot use strict mode. Declared handler failures produce isError results, while parameter validation fails with InvalidParams. + +- `McpServer.resource` -> `McpServer.resource`: Moved to effect/unstable/ai/McpServer. The registration layer excludes McpSchema.McpRequestContext from resource and completion handler requirements instead of McpServerClient. + - `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. +- `McpServer.toolkit` -> `McpServer.toolkit`: Moved to effect/unstable/ai/McpServer. The registration layer supplies McpSchema.McpRequestContext to handlers instead of excluding McpServerClient from requirements. Strict tools reject excess properties and require an Effect Schema rather than raw dynamic JSON Schema. + ### `@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. @@ -4994,6 +5030,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Response.ToolCallPartEncoded` -> `Response.ToolCallPartEncoded`: Moved to effect/unstable/ai/Response; providerName was removed while providerExecuted remains optional when encoded. +- `Response.ToolResultPart` -> `Response.ToolResultPart`: The schema selects success or failure using isFailure rather than trying both result schemas. It returns Schema.Codec; supply decoding services when decoding and encoding services when encoding. + - `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. @@ -5048,6 +5086,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Tool.Requirements` -> `Tool.HandlerServices`: Renamed and refined. HandlerServices combines parameter-decoding, result-encoding, and request-level dependencies required by a tool handler. +- `Tool.Result` -> `Tool.Result`: The result includes success, declared failure, and execution-denied or execution-interrupted values in both failure modes. Return mode also includes AiError; handle these variants when narrowing results. + +- `Tool.ResultEncoded` -> `Tool.ResultEncoded`: The encoded result includes execution-denied and execution-interrupted variants in both failure modes, plus encoded AiError in return mode. + - `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. @@ -5080,9 +5122,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.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.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. @@ -5096,9 +5138,25 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.boolean` -> `Flag.Boolean / Argument.ChoiceWithValue`: Positional booleans were removed as ambiguous; prefer a boolean flag or explicit true/false positional choices. + +- `Args.choice` -> `Argument.Literals`: Use the renamed constructor and pass the argument name explicitly. + +- `Args.date` -> `Argument.Date`: Use the renamed constructor and pass the argument name explicitly. + +- `Args.directory` -> `Argument.Directory`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. + +- `Args.file` -> `Argument.File`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. + +- `Args.fileContent` -> `Argument.File + Argument.mapEffect`: Parse a path and read it with FileSystem.readFile; no binary-content argument constructor remains. + +- `Args.fileParse` -> `Argument.FileParse`: Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple. + +- `Args.fileSchema` -> `Argument.FileSchema`: Pass the old format as an options field and use a v4 Schema constraint decoder. -- `Args.fileContent` -> `Argument.file + Argument.mapEffect`: Parse a path and read it with FileSystem.readFile; no binary-content argument constructor remains. +- `Args.fileText` -> `Argument.FileText`: Use the renamed constructor; it returns content only. + +- `Args.float` -> `Argument.Finite`: Use the renamed constructor; it rejects non-finite numbers. - `Args.getHelp` -> `none`: Per-argument help introspection was removed; Command generates help internally. @@ -5110,19 +5168,27 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Args.getUsage` -> `none`: The public Usage tree was removed; Command generates a usage string internally. +- `Args.integer` -> `Argument.Int`: Use the renamed constructor and pass the argument name explicitly. + - `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.mapEffect`: TODO: needs guidance +- `Args.none` -> `omit the config entry`: V4 Argument.Never is an always-failing sentinel, not v3's empty successful argument set. + - `Args.optional` -> `Argument.optional`: Use the moved combinator; it still returns Option. +- `Args.path` -> `Argument.Path`: Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement. + +- `Args.redacted` -> `Argument.Redacted`: Use the renamed constructor and pass the argument name explicitly. + - `Args.repeated` -> `Argument.variadic`: Renamed to variadic; pass optional min and max bounds. -- `Args.secret` -> `Argument.redacted`: Use Redacted-backed positional input. +- `Args.secret` -> `Argument.Redacted`: Use Redacted-backed positional input. -- `Args.text` -> `Argument.string`: Renamed to string; pass the argument name explicitly. +- `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. @@ -5284,7 +5350,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.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. @@ -5364,7 +5430,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.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. @@ -5380,29 +5446,29 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Options.between` -> `Flag.between`: Use the moved combinator; v4 validates bounds when constructing the parameter. -- `Options.boolean` -> `Flag.boolean + Flag.withDefault`: Use Flag.boolean(name).pipe(Flag.withDefault(false)) to preserve v3's omitted-flag default; bare Flag.boolean is now required. --no-name is automatic and aliases are added with Flag.withAlias. +- `Options.boolean` -> `Flag.Boolean + Flag.withDefault`: Use Flag.Boolean(name).pipe(Flag.withDefault(false)) to preserve v3's omitted-flag default; bare Flag.Boolean is now required. --no-name is automatic and aliases are added with Flag.withAlias. -- `Options.choice` -> `Flag.choice`: Use the moved constructor. +- `Options.choice` -> `Flag.Literals`: Use the moved constructor. -- `Options.choiceWithValue` -> `Flag.choiceWithValue`: Use the moved constructor. +- `Options.choiceWithValue` -> `Flag.ChoiceWithValue`: Use the moved constructor. -- `Options.date` -> `Flag.date`: 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.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.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.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.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.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.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.float` -> `Flag.Finite`: Use the moved constructor. - `Options.getHelp` -> `none`: Per-flag help introspection was removed; Command generates help internally. @@ -5410,13 +5476,13 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Options.getUsage` -> `none`: The public Usage tree was removed; Command generates a usage string internally. -- `Options.integer` -> `Flag.integer`: Use the moved constructor. +- `Options.integer` -> `Flag.Int`: 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.keyValueMap` -> `Flag.KeyValuePair`: Renamed and now returns Record\ rather than HashMap. - `Options.map` -> `Flag.map`: Use the moved combinator. @@ -5424,7 +5490,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.none` -> `omit the config entry`: V4 Flag.Never is an always-failing sentinel, not v3's empty successful option set. - `Options.optional` -> `Flag.optional`: Use the moved combinator; it still returns Option. @@ -5436,13 +5502,13 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.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.secret` -> `Flag.Redacted`: The deprecated Secret constructor was removed; use Redacted-backed input. -- `Options.text` -> `Flag.string`: Renamed from text to string. +- `Options.text` -> `Flag.String`: Renamed from text to String. - `Options.withAlias` -> `Flag.withAlias`: Use the moved combinator. @@ -5468,19 +5534,23 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.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.choice` -> `Primitive.choice`: Use the moved constructor. +- `Primitive.date` -> `Primitive.Date`: Date is now a singleton Primitive value. -- `Primitive.date` -> `Primitive.date`: Date is now a singleton Primitive value. +- `Primitive.float` -> `Primitive.Finite`: Finite is now a singleton Primitive value and rejects non-finite numbers. - `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.integer` -> `Primitive.Int`: Int is now a singleton Primitive value. + - `Primitive.isBool` -> `none`: The boolean Primitive predicate is internal in v4. -- `Primitive.text` -> `Primitive.string`: Renamed from text to string. +- `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. @@ -5492,21 +5562,41 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Prompt.Prompt` -> `Prompt.Prompt`: The model moved to effect/unstable/cli; quitting now fails with Terminal.QuitError. +- `Prompt.Prompt.FloatOptions` -> `Prompt.NumberOptions`: Use the renamed public options type for Prompt.Number; it still extends the integer options type, now IntOptions. + +- `Prompt.Prompt.IntegerOptions` -> `Prompt.IntOptions`: Use the renamed public options type for Prompt.Int; option fields are preserved. + - `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.confirm` -> `Prompt.Confirm`: Use the renamed constructor. + +- `Prompt.custom` -> `Prompt.Custom`: Use the renamed constructor; both overloads are preserved. + +- `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.Number`: Use the moved constructor; v4 also supports a default value. + +- `Prompt.hidden` -> `Prompt.Hidden`: Use the renamed constructor. -- `Prompt.file` -> `Prompt.file`: Use the moved constructor; v4 also supports a default selected path. +- `Prompt.integer` -> `Prompt.Int`: Use the moved constructor; v4 also supports a default value. -- `Prompt.float` -> `Prompt.float`: Use the moved constructor; v4 also supports a default value. +- `Prompt.list` -> `Prompt.List`: Use the renamed constructor. -- `Prompt.integer` -> `Prompt.integer`: Use the moved constructor; v4 also supports a default value. +- `Prompt.multiSelect` -> `Prompt.MultiSelect`: Use the renamed constructor. -- `Prompt.text` -> `Prompt.text`: Use the moved constructor. +- `Prompt.password` -> `Prompt.Password`: Use the renamed constructor. + +- `Prompt.select` -> `Prompt.Select`: Use the renamed constructor. + +- `Prompt.text` -> `Prompt.String`: Use the moved constructor. + +- `Prompt.toggle` -> `Prompt.Toggle`: Use the renamed constructor. ### `@effect/cli/ValidationError` @@ -5636,9 +5726,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.layerHttpApi` -> `effect/unstable/cluster/EntityProxyServer#layerHttpApi`: Moved into core Effect. Use v4 HttpApi identifiers and provide Sharding plus both Rpc.ServicesServer and Rpc.ServicesClient codec requirements. -- `EntityProxyServer.layerRpcHandlers` -> `effect/unstable/cluster/EntityProxyServer#layerRpcHandlers`: Moved into core Effect; the service requirement is now Rpc.ServicesServer rather than Rpc.Context. +- `EntityProxyServer.layerRpcHandlers` -> `effect/unstable/cluster/EntityProxyServer#layerRpcHandlers`: Moved into core Effect; replace Rpc.Context with both Rpc.ServicesServer and Rpc.ServicesClient codec requirements, alongside Sharding. ### `@effect/cluster/EntityResource` @@ -5692,6 +5782,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/cluster/Reply` +- `Reply.Reply` -> `Reply.Reply(rpc, codecFor)`: Pass the transport codec. Decoding replies requires Rpc.ServicesClient; encoding replies requires Rpc.ServicesServer. + - `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, codecFor) with the transport's codec. - `Reply.TypeId` -> `none`: The reply marker is private in v4. Use Reply.isReply for runtime refinement. @@ -5866,8 +5958,6 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `EventLogRemote.Hello` -> `effect/unstable/eventlog/EventLogMessage#HelloResponse`: HelloResponse replaces Hello and includes the v4 authentication challenge; HelloRpc defines the endpoint. -- `EventLogRemote.Ping`: TODO: needs guidance - - `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. @@ -5994,12 +6084,14 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@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` -> `effect/unstable/reactivity/Reactivity#Reactivity`: Use the Reactivity interface for implementations and the Reactivity Context.Service value as the service key. Implementations require the TypeId brand; prefer Reactivity.make. 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.Reactivity.Service` -> `effect/unstable/reactivity/Reactivity#Reactivity`: The named namespace member was removed; use the branded Reactivity interface directly. Prefer Reactivity.make, or include [Reactivity.TypeId]: Reactivity.TypeId in a custom implementation. - `Reactivity.make` -> `effect/unstable/reactivity/Reactivity#make`: Import make from the v4 unstable Reactivity module. +- `Reactivity.query`: TODO: needs guidance + ### `@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. @@ -6192,7 +6284,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@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.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 and onError. The ignored bufferSize option was removed. - `BunStream.FromWritableOptions` -> `Pick[0], "endOnDone" | "encoding">`: The named interface was inlined into BunSink.fromWritable and duplex constructor options. @@ -6336,6 +6428,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Undici.MessageEventInit` -> `undici.MessageEventInit`: Import the upstream type directly; message ports and source use MessagePort instances in Undici 8. +- `Undici.MockAgent`: TODO: needs guidance + - `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(...). @@ -6438,6 +6532,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Cookies.CookieTypeId` -> `Cookies.isCookie`: The cookie brand is private in v4; use the public refinement instead of reading the type-id symbol. +- `Cookies.CookiesError` -> `Cookies.CookiesError`: The error tag is CookiesError rather than CookieError. Update catchTag calls and \_tag comparisons; validation details are in the reason field. + - `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. @@ -6486,21 +6582,35 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `FileSystem.FileTypeId` -> `typeof FileSystem.FileTypeId`: The runtime marker remains exported, but the separate type alias was removed. +- `FileSystem.GiB` -> `ByteSize.gibibytes`: Use the ByteSize binary unit constructor. + +- `FileSystem.KiB` -> `ByteSize.kibibytes`: Use the ByteSize binary unit constructor. + - `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.MiB` -> `ByteSize.mebibytes`: Use the ByteSize binary unit constructor. + - `FileSystem.OpenFileOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. +- `FileSystem.PiB` -> `ByteSize.pebibytes`: Use the ByteSize binary unit constructor. + - `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.Size` -> `ByteSize.ByteSize`: Use ByteSize.bytes or unit constructors for file sizes. Truncation lengths, buffer sizes, and read/write counts use number. File.seek takes and returns signed bigint positions; it can fail with PlatformError, including BadArgument when seeking before the start. + +- `FileSystem.SizeInput` -> `ByteSize.Input`: File-size and path-backed range inputs use ByteSize.Input. String inputs must be non-negative decimal integers without leading zeros, followed by a canonical unit symbol or lowercase unit name, with at most one separating space. Parse external strings or fractional quantities with ByteSize.fromString (Option) or ByteSize.fromStringUnsafe (throws), then pass the resulting ByteSize. Truncation lengths, Web File ranges, and buffer sizes use number. + +- `FileSystem.StreamOptions` -> `NonNullable[1]>`: Stream options are inline; bufferSize was removed, bytesToRead and offset accept ByteSize inputs, and chunkSize uses number. + +- `FileSystem.TiB` -> `ByteSize.tebibytes`: Use the ByteSize binary unit constructor. - `FileSystem.WatchEventCreate` -> `FileSystem.WatchEvent.Create`: The constructor was removed; construct a tagged object with \_tag: "Create" and path. @@ -6528,9 +6638,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.schema` -> `Schema.Headers`: The encoded-record and self schemas were consolidated and moved to effect/Schema as Schema.Headers. -- `Headers.schemaFromSelf` -> `Headers.HeadersSchema`: The encoded-record and self schemas were consolidated into HeadersSchema. +- `Headers.schemaFromSelf` -> `Schema.Headers`: The encoded-record and self schemas were consolidated and moved to effect/Schema as Schema.Headers. - `Headers.unsafeFromRecord` -> `Headers.fromRecordUnsafe`: Renamed to put Unsafe last; it still skips name normalization. @@ -6558,6 +6668,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `HttpApiBuilder.HandlersTypeId` -> `none`: The exported symbol was removed; do not inspect or construct the private Handlers marker. +- `HttpApiBuilder.Middleware` -> `none`: The API-specific middleware service tag was removed. Declared HttpApiMiddleware services are applied while routes are built; use HttpRouter.middleware for additional global middleware. + - `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. @@ -6568,7 +6680,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.handler` -> `effect/unstable/httpapi/HttpApiBuilder#handler`: The typed callback helper remains; names are now identifiers and API/group global error channels are gone. Pass the returned callback 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. @@ -6878,9 +6990,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.file` -> `HttpBody.file`: Retained; bufferSize was replaced by numeric chunkSize. Offset and bytesToRead accept ByteSize.Input. Invalid ranges and a final EOF-clamped content length above Number.MAX\_SAFE\_INTEGER fail with PlatformError / BadArgument. -- `HttpBody.fileInfo` -> `HttpBody.fileFromInfo`: Renamed; it still uses supplied File.Info for content length and requires FileSystem. +- `HttpBody.fileInfo` -> `HttpBody.fileFromInfo`: Renamed; it uses supplied File.Info with ByteSize size metadata and requires FileSystem. Offset and bytesToRead accept ByteSize.Input, while chunkSize is numeric. Invalid ranges and a final EOF-clamped content length above Number.MAX\_SAFE\_INTEGER fail with PlatformError / BadArgument. - `HttpBody.unsafeJson` -> `HttpBody.jsonUnsafe`: Renamed to put Unsafe last; serialization failures still throw. @@ -6952,6 +7064,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/HttpClientResponse` +- `HttpClientResponse.HttpClientResponse` -> `HttpClientResponse.HttpClientResponse`: Custom implementations must supply the required url string. It represents the resolved URL including query parameters and excluding the hash, uses the final URL after redirects, and is empty when unknown. + - `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. @@ -6972,11 +7086,11 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/HttpIncomingMessage` -- `HttpIncomingMessage.MaxBodySize` -> `HttpIncomingMessage.MaxBodySize`: Changed from a Reference subclass holding Option\ to Context.Reference\. +- `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. +- `HttpIncomingMessage.withMaxBodySize` -> `Effect.provideService(HttpIncomingMessage.MaxBodySize, size)`: The helper was removed; provide a ByteSize value or undefined directly. ### `@effect/platform/HttpLayerRouter` @@ -7046,13 +7160,13 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/HttpPlatform` -- `HttpPlatform.HttpPlatform` -> `HttpPlatform.HttpPlatform`: The service is now a Context.Service class; use its Service member for the implementation type. +- `HttpPlatform.HttpPlatform` -> `HttpPlatform.HttpPlatform`: The service is now a Context.Service class; use its Service member for the implementation type. Path-backed offset and bytesToRead accept ByteSize.Input, while chunkSize and all Web File range options use number. - `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. +- `HttpPlatform.make` -> `HttpPlatform.make`: Retained; v4 returns the service implementation. The fileResponse callback receives contentLength as bigint, while start and end remain numbers. Web File range options use number. ### `@effect/platform/HttpRouter` @@ -7124,17 +7238,25 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/HttpServer` +- `HttpServer.Address` -> `effect/unstable/net/NetAddress#SocketAddress`: Replaced by the shared concrete internet-or-Unix socket address union. + - `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.TcpAddress` -> `effect/unstable/net/NetAddress#InetAddress`: Replaced by the shared resolved internet-address model; use address and port instead of hostname and port. + - `HttpServer.TypeId` -> `none`: The public TypeId was removed; HttpServer is now a Context.Service class. +- `HttpServer.UnixAddress` -> `effect/unstable/net/NetAddress#UnixPathAddress`: Replaced by the shared Unix filesystem-path address model. + - `HttpServer.addressWith` -> `HttpServer.HttpServer.use(({ address }) => effect(address))`: The accessor was removed; read the service and pass its Address to the callback. +- `HttpServer.formatAddress` -> `HttpServer.formatAddress`: Accepts NetAddress.SocketAddress and delegates to NetAddress.formatUrlUnsafe. Scoped IPv6 addresses throw NetAddressError; Unix paths use the unix:// prefix. + - `HttpServer.layerContext` -> `HttpServer.layerServices`: Renamed; it provides the standard HTTP platform services. -- `HttpServer.make` -> `HttpServer.make`: Retained; it returns the Context.Service implementation. +- `HttpServer.make` -> `HttpServer.make`: Retained; it returns the Context.Service implementation. Supply a NetAddress.SocketAddress for address, using NetAddress.inetAddressUnsafe for a parsed IP and port or NetAddress.unixPathAddress for a Unix path. Use NetAddress.inetAddress to validate the port through Result instead. - `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. @@ -7180,7 +7302,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.file` -> `HttpServerResponse.file`: Retained; offset and bytesToRead accept ByteSize.Input, while chunkSize uses number. Path-backed responses validate ranges and clamp content length to the available bytes. + +- `HttpServerResponse.fileWeb` -> `HttpServerResponse.fileWeb`: Web File offset, bytesToRead, and chunkSize options use number, unlike path-backed ByteSize.Input ranges. - `HttpServerResponse.isServerResponse` -> `HttpServerResponse.isHttpServerResponse`: Renamed. @@ -7190,7 +7314,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `HttpServerResponse.text` -> `HttpServerResponse.text`: Moved unchanged. -- `HttpServerResponse.toWeb` -> `HttpServerResponse.toWeb`: Retained, but the optional Runtime became an optional Context for stream execution. +- `HttpServerResponse.toWeb` -> `HttpServerResponse.toWeb`: Retained, but the optional Runtime became an optional Context for stream execution. For a raw Web Response, outer headers override native headers and cookies append, including when the body is omitted. With withoutBody, the raw status and statusText are preserved unless the outer status is 204, 205, or 304, which takes precedence. - `HttpServerResponse.uint8Array` -> `HttpServerResponse.uint8Array`: Moved unchanged. @@ -7244,9 +7368,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.MaxFieldSize` -> `Multipart.MaxFieldSize`: Now a Context.Reference\; provide a value such as ByteSize.bytes(100). -- `Multipart.MaxFileSize` -> `Multipart.MaxFileSize`: The setting remains as a Context.Reference; use undefined rather than Option.none for no limit. +- `Multipart.MaxFileSize` -> `Multipart.MaxFileSize`: Now a Context.Reference\; provide ByteSize.bytes(100), for example, or undefined for no limit. - `Multipart.MaxParts` -> `Multipart.MaxParts`: The setting remains as a Context.Reference; use undefined rather than Option.none for no limit. @@ -7260,13 +7384,13 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.withLimits.Options` -> `Multipart.withLimits.Options`: Limit fields now use optional plain numbers or ByteSize inputs; 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.withMaxFieldSize` -> `Effect.provideService(Multipart.MaxFieldSize, size)`: Provide a ByteSize value, such as ByteSize.bytes(100). To normalize ByteSize.Input options, use Multipart.limitsServices. -- `Multipart.withMaxFileSize` -> `Effect.provideService(Multipart.MaxFileSize, size)`: Provide the v4 Context.Reference around the effect, converting Option.none to undefined. +- `Multipart.withMaxFileSize` -> `Effect.provideService(Multipart.MaxFileSize, size)`: Replace Option.none with undefined and Option.some(value) with a normalized ByteSize value. To normalize ByteSize.Input options, use Multipart.limitsServices. - `Multipart.withMaxParts` -> `Effect.provideService(Multipart.MaxParts, count)`: Provide the v4 Context.Reference around the effect, converting Option.none to undefined. @@ -7318,6 +7442,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `OpenApiJsonSchema.Array` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the general object model. +- `OpenApiJsonSchema.Boolean` -> `effect/JsonSchema#JsonSchema`: The narrow boolean interface was consolidated into the open, dialect-neutral JSON Schema 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. @@ -7328,6 +7454,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `OpenApiJsonSchema.JsonSchema` -> `effect/JsonSchema#JsonSchema`: Use the dialect-neutral open JSON Schema object model. +- `OpenApiJsonSchema.Never` -> `effect/JsonSchema#JsonSchema`: The special never-schema interface was consolidated into the open JSON Schema object model; represent it with a not constraint. + +- `OpenApiJsonSchema.Number` -> `effect/JsonSchema#JsonSchema`: The narrow number interface was consolidated into the open, dialect-neutral 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. @@ -7336,8 +7466,12 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `OpenApiJsonSchema.Root` -> `effect/JsonSchema#MultiDocument`: OpenAPI generation keeps roots in schemas and shared components in definitions; the inline-definitions root model is gone. +- `OpenApiJsonSchema.String` -> `effect/JsonSchema#JsonSchema`: The narrow string interface was consolidated into the open, dialect-neutral JSON Schema object model. + - `OpenApiJsonSchema.Unknown`: TODO: needs guidance +- `OpenApiJsonSchema.Void` -> `effect/JsonSchema#JsonSchema`: The special void-schema interface was consolidated into the open JSON Schema object model. + - `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. @@ -7390,8 +7524,14 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/SocketServer` +- `SocketServer.Address` -> `effect/unstable/net/NetAddress#SocketAddress`: Replaced by the shared concrete internet-or-Unix socket address union. + - `SocketServer.ErrorTypeId` -> `SocketServer.ErrorTypeId`: The API moved to effect/unstable/socket/SocketServer and retains this name. +- `SocketServer.TcpAddress` -> `effect/unstable/net/NetAddress#InetAddress`: Replaced by the shared resolved internet-address model; use address and port instead of hostname and port. + +- `SocketServer.UnixAddress` -> `effect/unstable/net/NetAddress#UnixPathAddress`: Replaced by the shared Unix filesystem-path address model. + ### `@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. @@ -7416,6 +7556,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/platform/Url` +- `Url.fromString` -> `Url.fromString`: Retained; returns Result with IllegalArgumentError instead of Either with IllegalArgumentException. + - `Url.setUrlParams` -> `Url.setUrlParams`: Retained and widened to accept UrlParams.Input. ### `@effect/platform/UrlParams` @@ -7424,19 +7566,21 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `UrlParams.Input` -> `UrlParams.Input`: Retained and broadened to include UrlParams itself. +- `UrlParams.UrlParams` -> `UrlParams.UrlParams`: Import UrlParams from effect/unstable/http. It is now a branded iterable object with a params field rather than a ReadonlyArray; construct it with UrlParams.make or UrlParams.fromInput. + - `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.schemaFromSelf` -> `Schema.UrlParams`: The declaration schema for the v4 wrapper moved to effect/Schema. -- `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.schemaFromString` -> `Schema.String.pipe(Schema.decodeTo(Schema.UrlParams, { 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.schemaJson` -> `Schema.JsonFromUrlParamsField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect)`: The field codec moved to effect/Schema. Compose it 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.schemaParse` -> `UrlParamsFromString.pipe(Schema.decodeTo(Schema.RecordFromUrlParams.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.schemaRecord` -> `Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema))`: RecordFromUrlParams is a base codec in effect/Schema; 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.schemaStruct` -> `Schema.RecordFromUrlParams.pipe(Schema.decodeTo(schema), Schema.decodeEffect)`: Compose the record codec from effect/Schema with the target schema and decode it. - `UrlParams.toString` -> `UrlParams.toString`: Retained and broadened to accept any UrlParams.Input. @@ -7790,9 +7934,9 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@effect/sql-pg/PgClient` -- `PgClient.PgClient` -> `@effect/sql-pg/PgClient#PgClient`: Retained; the service value is now a Context.Service. +- `PgClient.PgClient` -> `@effect/sql-pg/PgClient#PgClient`: Retained; the service value is now a Context.Service. The listen method returns a scoped Effect acquiring a Queue.Dequeue\; consume it with Queue operations or Stream.fromQueue and read each notification's payload. Connection failures after registration fail the queue with the original SqlError; scope closure interrupts consumers. -- `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.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. The types option takes a PgTypes.Registry rather than node-pg custom types. Timestamp and timestamptz results (including array elements) are Date values with millisecond precision; infinity, -infinity, and out-of-range values become invalid Dates. Numeric readers can use getTime() or register numeric codecs. Date parameters bind as timestamptz; use PgTypes.timestamp(value) to preserve UTC fields when writing a timestamp column instead of applying the session TimeZone. - `PgClient.PgClientFromPoolOptions` -> `none`: The node-pg Pool wrapper options were removed with fromPool. Use PgClient.PgPoolConfig with PgClient.make or PgClient.layer. @@ -7818,6 +7962,10 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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/OpfsWorker` + +- `OpfsWorker.OpfsWorkerConfig`: TODO: needs guidance + ### `@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. @@ -7834,6 +7982,8 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `Model.Class` -> `effect/unstable/schema/Model#Class`: Moved; model variants remain select, insert, update, json, jsonCreate, and jsonUpdate. +- `Model.Date` -> `effect/unstable/schema/Model#Date`: Moved; still serializes DateTime.Utc as a YYYY-MM-DD string. + - `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. @@ -7844,7 +7994,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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. +- `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. The insert resolver requires model decoding services as well as insert-schema encoding services. ### `@effect/sql/SqlClient` @@ -8294,25 +8444,41 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ### `@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.ApiConfig` -> `vitest/node#ApiConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.Assertion` -> `vitest#Assertion`: Vitest 5 takes the matcher return type first. Replace Assertion\ with Assertion\ or Assertion\, T\> for asynchronous assertions. + - `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.BaseCoverageOptions` -> `vitest/node#BaseCoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. + +- `index.BenchFactory` -> `vitest#Bench`: Use the Vitest 5 test-context bench fixture type. It is no longer the tinybench factory constructor. + +- `index.BenchFunction` -> `vitest#BenchFn`: Use BenchFn for the callback passed to the Vitest 5 test-context bench fixture. + +- `index.BenchTask` -> `vitest#BenchRegistration`: Migrate to a fixture registration and await its run() method; review its fields instead of treating it as a tinybench task. + +- `index.BenchTaskResult` -> `vitest#BenchResult`: Use the result returned by awaiting the Vitest 5 fixture registration's run() method. -- `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.Benchmark` -> `vitest#TestBenchmark`: Use TestBenchmark for recorded benchmark data on a test; benchmarks are no longer standalone test tasks. -- `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.BenchmarkAPI` -> `vitest#Bench`: Use the test-context bench fixture. Move skip, only, and todo to the enclosing test. -- `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.BenchmarkResult` -> `vitest#BenchResult`: Use the result returned by awaiting the Vitest 5 fixture registration's run() method; review its changed fields. -- `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.BenchmarkUserOptions` -> `vitest/node#BenchmarkUserOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.BrowserConfigOptions` -> `vitest/node#BrowserConfigOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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 5 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 5 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 5 shape. - `index.CollectLineNumbers` -> `vitest/node#TypeCheckCollectLineNumbers`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. @@ -8322,97 +8488,101 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.CoverageIstanbulOptions` -> `vitest/node#CoverageIstanbulOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.CoverageOptions` -> `vitest/node#CoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.CoverageProvider` -> `vitest/node#CoverageProvider`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.CoverageProviderModule` -> `vitest/node#CoverageProviderModule`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.CoverageReporter` -> `vitest/node#CoverageReporter`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.CoverageV8Options` -> `vitest/node#CoverageV8Options`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.CustomProviderOptions` -> `vitest/node#CustomProviderOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.DepsOptimizationOptions` -> `vitest/node#DepsOptimizationOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.Environment` -> `vitest/runtime#Environment`: This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime. -- `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.EnvironmentOptions` -> `vitest/node#EnvironmentOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.EnvironmentReturn` -> `vitest/runtime#EnvironmentReturn`: This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime. - `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.ExpectPollOptions` -> `NonNullable[1]>`: Vitest 5 removes the named options export; derive the options from the public expect.poll function. + +- `index.ExtendedContext` -> `vitest#TestContext`: The separate context alias was removed. Vitest 5 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.HappyDOMOptions` -> `NonNullable`: Vitest 5 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.HookCleanupCallback` -> `none`: No named Vitest 5 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.HookListener` -> `none`: Infer the callback from the public hook function, or derive it with Parameters\[0] and the corresponding hook name. -- `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.InlineConfig` -> `vitest/node#InlineConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.JSDOMOptions` -> `NonNullable`: Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type. +- `index.JSDOMOptions` -> `NonNullable`: Vitest 5 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type. + +- `index.Matchers` -> `vitest#Matchers`: Augment vitest.Matchers\ for custom matchers. R is the matcher return type and T is the received value; @vitest/expect no longer shares Vitest's assertion state. - `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.ModuleCache` -> `none`: Vitest 3 marked this unused internal cache shape deprecated; Vitest 5 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.Pool` -> `vitest/node#Pool`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.PoolOptions` -> `vitest/config#TestUserConfig`: The v3 built-in poolOptions object was removed. Move its fields to Vitest 5 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.ProjectConfig` -> `vitest/node#ProjectConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.ReportContext` -> `vitest/node#ReportContext`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.Reporter` -> `vitest/reporters#Reporter`: Import Reporter from the public plural vitest/reporters entrypoint; its lifecycle methods changed in Vitest 4. +- `index.Reporter` -> `vitest/node#Reporter`: Import Reporter from vitest/node; the deprecated vitest/reporters entrypoint was removed in Vitest 5. - `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.ResolvedConfig` -> `vitest/node#ResolvedConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.ResolvedCoverageOptions` -> `vitest/node#ResolvedCoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.ResolvedTestEnvironment` -> `none`: Vitest 3 marked this type unsupported. Use Environment from vitest/environments for custom environments. +- `index.ResolvedTestEnvironment` -> `none`: Vitest 3 marked this type unsupported. Use Environment from vitest/runtime 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.RuntimeContext` -> `none`: Vitest 5 deprecates @vitest/runner and does not expose this internal state type. Extend TestRunner from vitest and use its public methods instead. -- `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.SequenceHooks` -> `vitest/node#SequenceHooks`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.SequenceSetupFiles` -> `vitest/node#SequenceSetupFiles`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.SuiteHooks` -> `ReturnType`: Derive the hook collection from Vitest 5's public TestRunner API; 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.TaskContext` -> `vitest#TestContext`: The separate context alias was removed. Vitest 5 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. @@ -8424,32 +8594,40 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.TypecheckConfig` -> `vitest/node#TypecheckConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 shape. -- `index.UserConfig` -> `vitest/config#TestUserConfig`: Vitest 4 exposes its config as TestUserConfig; ViteUserConfig is the separate Vite configuration type. +- `index.UserConfig` -> `vitest/config#TestUserConfig`: Vitest 5 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.VitestEnvironment` -> `vitest/node#VitestEnvironment`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.VitestRunMode` -> `vitest/node#VitestRunMode`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.VmEnvironmentReturn` -> `vitest/runtime#VmEnvironmentReturn`: This was a deprecated root re-export. Import it from vitest/runtime; Vitest 5 exposes custom environments through vitest/runtime. -- `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.WorkerContext` -> `vitest/node#WorkerContext`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 5 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.bench` -> `vitest#test`: Vitest 5 removes the top-level bench export. Destructure bench from a regular test's context and await bench(name, fn).run(); use skip, only, or todo on the enclosing test. + - `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.describe` -> `vitest#describe`: Vitest 5 removes describe.sequential and sequential options. Use describe(name, { concurrent: false }, body) for suites that depend on ordering. + - `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.it` -> `@effect/vitest#it`: Effect helpers retain their calling convention. Vitest 5 removes it.sequential; pass { concurrent: false } to the native test or as the Effect helper's third argument. + - `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. +- `index.test` -> `vitest#test`: Vitest 5 removes test.sequential and sequential options. Pass { concurrent: false } to opt out of inherited concurrency. + ### `@effect/vitest/utils` - `utils.assertFailure` -> `assertExitFailure`: For v3 Exit values, rename to assertExitFailure. In v4, assertFailure instead asserts Result.Failure. @@ -8494,7 +8672,7 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.into` -> `effect/unstable/workflow/DurableDeferred#into`: Moved into core Effect with the same exit recording and suspension propagation behavior. Provide both decoding and encoding services for the success and error schemas; recording the exit requires encoding services. - `DurableDeferred.make` -> `effect/unstable/workflow/DurableDeferred#make`: Moved into core Effect with the same name and optional schemas, expressed through v4 Schema.Constraint. @@ -8516,8 +8694,6 @@ effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) - `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.Complete`: TODO: needs guidance - - `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. @@ -8998,9 +9174,9 @@ Arbitrary.schema(schema) - `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.run` -> `Channel.runDrain`: Use runDrain to consume all emitted elements and return the channel's done value. -- `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.runScoped` -> `Channel.toPull`: No direct scoped runner remains. Use toPull in the caller scope and recover Cause.Done; use 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. @@ -9900,7 +10076,7 @@ Arbitrary.schema(schema) - `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.try` -> `Effect.try`: Use the callback overload for Cause.UnknownError, or the object overload with try and catch to map failures to a custom error. The callback-only overload does not accept a custom error type parameter. - `Effect.tryMap` -> `Effect.flatMap + Effect.try`: FlatMap the source value into the v4 synchronous try constructor. Adapt arguments and imports to the v4 API. @@ -9990,7 +10166,7 @@ Arbitrary.schema(schema) - `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.Class` -> `Effectable.Class`: Still available; replace commit() with an asEffect() method returning the Effect. The intermediate v4 override property/getter is no longer supported. - `Effectable.CommitPrimitive` -> `new() => Effect.Effect`: The named constructor interface was removed; inline the constructor type when needed. @@ -10004,7 +10180,7 @@ Arbitrary.schema(schema) - `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.StructuralClass` -> `Effectable.Class`: Use Class and migrate commit() to asEffect(); 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. @@ -10366,6 +10542,8 @@ Arbitrary.schema(schema) - `FastCheck.ShuffledSubarrayConstraints`: TODO: needs guidance +- `FastCheck.Size`: TODO: needs guidance + - `FastCheck.SizeForArbitrary`: TODO: needs guidance - `FastCheck.SparseArrayConstraints`: TODO: needs guidance @@ -11148,9 +11326,9 @@ FastCheck.uuid({ version: 4 }) - `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.MutableGraph` -> `Graph.MutableGraph`: The mutable type remains but no longer shares a public base interface with immutable Graph; 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.Proto` -> `none`: The common graph protocol was removed. Use Graph.Graph or Graph.MutableGraph as appropriate and replace storage-field access with public graph query and mutation functions. - `Graph.SearchConfig` -> `Graph.SearchConfig`: The type remains; direction is now Graph.TraversalDirection and also accepts undirected, while radius limits traversal depth. @@ -11295,7 +11473,7 @@ stream.pipe( **Replacement:** `Schema.toJsonSchemaDocument` -Wrap a low-level AST with Schema.make, then generate a document; v4 generation targets draft 2020-12. +Wrap a low-level AST with Schema.make, then generate a draft 2020-12 document. Generation defaults to onExcessProperty: 'ignore'; use 'error' for closed objects. JSON Schema validation is approximate and does not replace Effect decoding. **Example** @@ -11307,7 +11485,7 @@ Schema.toJsonSchemaDocument(Schema.make(ast)) **Replacement:** `Schema.toJsonSchemaDocument` -Generate draft 2020-12, then call JsonSchema.toDocumentDraft07 when draft-07 output is required. +Generate draft 2020-12, then call JsonSchema.toDocumentDraft07 when draft-07 output is required. Generation now defaults to onExcessProperty: 'ignore'; pass 'error' to retain closed-object output, and use the same option when decoding to reject excess properties. The earlier v4 additionalProperties generation option was removed. JSON Schema validation is approximate and does not replace Effect decoding. **Example** @@ -11449,7 +11627,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `Layer.setVersionMismatchErrorLogLevel` -> `none`: No version-mismatch log-level Reference or public replacement exists. -- `Layer.tapErrorCause` -> `Layer.tapCause`: The cause observer was renamed. +- `Layer.tapErrorCause` -> `Layer.tapCause`: The cause observer was renamed. Its callback must accept the source layer's full error cause; a callback narrowed to only part of the error union is rejected. - `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. @@ -11467,7 +11645,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `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` -> `LayerMap.Service`: Use layer instead of Default, layerNoDeps instead of DefaultWithoutDependencies, and contextEffect instead of runtime. Preloading does not remove acquisition errors from later lookups, which can reacquire expired or invalidated entries. - `LayerMap.Service.Context` -> `LayerMap.Service.Services`: The input-services extractor was renamed. @@ -11645,7 +11823,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `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.prettyLogger` -> `Logger.consolePretty`: Renamed to consolePretty. Remove the stderr option; provide Logger.LogToStderr with true to route TTY output to console.error. Colors, formatDate, and mode remain constructor options. - `Logger.prettyLoggerDefault` -> `Logger.consolePretty()`: The prebuilt singleton became a constructor call. @@ -11673,7 +11851,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `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.withSpanAnnotations` -> `custom Logger.make wrapper using options.fiber.cache.span`: No transparent generic equivalent remains. Read span identity from options.fiber.cache.span 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. @@ -12661,7 +12839,9 @@ 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.Composite` -> `SchemaIssue.Composite`: Composite parse failures moved to SchemaIssue. The v4 constructor takes the failing AST and an array of nested issues; input is retained only when reportInput is enabled. + +- `ParseResult.DeclarationDecodeUnknown` -> `SchemaGetter.Getter`: Custom declaration decoding now uses SchemaGetter values and Schema.declare annotations. Getter is a tagged union, not a constructor; use transformOptionalEffect for an Option-to-Effect decoder. Use standalone SchemaGetter.run, map, and compose instead of instance methods. - `ParseResult.DecodeUnknown` -> `Schema.decodeUnknownEffect`: Use the function type returned by Schema.decodeUnknownEffect. @@ -12677,10 +12857,14 @@ SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues - `ParseResult.ParseResultFormatter` -> `SchemaIssue.Formatter`: Issue formatter types moved to SchemaIssue. +- `ParseResult.Pointer` -> `SchemaIssue.Pointer`: Path-qualified failures moved to SchemaIssue. Construct them with the property path and nested issue; rejected input belongs to the nested issue when reportInput is enabled. + - `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.Transformation` -> `SchemaIssue.Encoding`: Transformation-stage failures are Encoding issues in v4. They retain the failing AST and nested issue; the old Encoded, Transformation, and Type kind discriminator was removed. + #### `ParseResult.TreeFormatter` **Replacement:** `SchemaIssue.defaultFormatter` @@ -13889,7 +14073,7 @@ Schema.toFormatter(schema) - `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.Filter` -> `Schema.Annotations.Filter`: Use the v4 filter annotation shape. arbitraryConstraint uses Schema.Annotations.ToArbitrary.FilterConstraint; the earlier v4 ToArbitrary.Constraint name was removed. - `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. @@ -14009,7 +14193,7 @@ Schema.toFormatter(schema) - `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.Enums` -> `Schema.Enum`: Rename the enum constructor and pass the enum object. Numeric members must be finite; NaN and positive or negative Infinity throw at construction. - `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. @@ -14377,9 +14561,9 @@ Schema.toFormatter(schema) - `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.TemplateLiteral` -> `Schema.TemplateLiteral(parts)`: Pass template literal parts as one array. Parts must not contain encodings, including inside unions and nested templates. Transformations whose decoded and encoded types are equal are also rejected. Use Schema.TemplateLiteralParser(parts) for transformed parts. -- `Schema.TemplateLiteralParser` -> `Schema.TemplateLiteralParser(schema.parts)`: Create the template schema first and pass its `parts` property. +- `Schema.TemplateLiteralParser` -> `Schema.TemplateLiteralParser(parts)`: Pass template literal parts directly as one array. Transformed parts are supported, and their decoding and encoding services are required in the corresponding direction. - `Schema.TimeZone` -> `Schema.TimeZoneFromString`: Use the string codec; v4 `TimeZone` is the self schema. @@ -14453,7 +14637,7 @@ Schema.toFormatter(schema) - `Schema.Union` -> `Schema.Union(members)`: Pass union members as one array. -- `Schema.UniqueSymbolFromSelf` -> `Schema.UniqueSymbol`: Use the v4 unique-symbol schema constructor. +- `Schema.UniqueSymbolFromSelf` -> `Schema.UniqueSymbol`: Use the v4 unique-symbol schema constructor. JSON encoding is available only for globally registered symbols and uses the exact String(symbol) value; local symbols cannot be encoded as JSON. - `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. @@ -14655,7 +14839,7 @@ Schema.toFormatter(schema) - `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.multipleOf` -> `Schema.isMultipleOf`: Rename the predicate to `isMultipleOf` and apply it with `Schema.check` or a schema's `check` method. The divisor must be finite and nonzero or construction throws RangeError; negative divisors are normalized to their absolute value. - `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. @@ -14755,7 +14939,7 @@ Schema.toFormatter(schema) - `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.transformOrFail` -> `schema.pipe(Schema.decodeTo(target, { decode: SchemaGetter.transformEffect(...), encode: ... }))`: Replace the constructor with `decodeTo` and effectful `SchemaGetter` transformations. - `Schema.trimmed` -> `Schema.isTrimmed`: Rename the string predicate to `isTrimmed` and apply it with `Schema.check` or a schema's `check` method. @@ -14783,13 +14967,13 @@ Schema.toFormatter(schema) - `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.Annotated` -> `SchemaAST.AST`: The public base type was removed. Use the AST union; every variant still exposes 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. Declarations use the toCodecArbitrary annotation; filters use arbitraryConstraint. -- `SchemaAST.BatchingAnnotation` -> `none`: Per-schema batching annotations were removed; control asynchronous parsing with ParseOptions.concurrency. +- `SchemaAST.BatchingAnnotation` -> `none`: Per-schema batching annotations were removed. They have no direct replacement. ParseOptions.concurrency controls product child parsing only; configure request batching separately. - `SchemaAST.BatchingAnnotationId` -> `none`: Symbol annotation IDs were removed and batching is no longer a schema annotation. @@ -14805,9 +14989,9 @@ Schema.toFormatter(schema) - `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.ConcurrencyAnnotation` -> `SchemaAST.ParseOptions`: Per-schema concurrency annotations were removed. Pass concurrency in runtime ParseOptions when creating or calling a decoder, encoder, or constructor. It applies independently to each tuple, array, struct, record, or struct-with-rest node and does not make Union candidates concurrent. -- `SchemaAST.ConcurrencyAnnotationId` -> `Schema.Annotations.Bottom["parseOptions"]`: Symbol annotation IDs were removed; put concurrency inside the parseOptions annotation. +- `SchemaAST.ConcurrencyAnnotationId` -> `SchemaAST.ParseOptions`: The annotation ID was removed. Pass concurrency in runtime ParseOptions; product parsing follows Effect.forEach concurrency semantics while Union candidates remain sequential. - `SchemaAST.Declaration` -> `SchemaAST.Declaration`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. @@ -14827,7 +15011,7 @@ Schema.toFormatter(schema) - `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.Enums` -> `SchemaAST.Enum`: Renamed to Enum in the v4 AST model. Numeric enum members must be finite; NaN and positive or negative Infinity throw at construction. - `SchemaAST.EquivalenceAnnotation` -> `Schema.Annotations.ToEquivalence.Declaration`: Equivalence derivation annotations now use the toEquivalence key in Schema.Annotations. @@ -14883,9 +15067,9 @@ Schema.toFormatter(schema) - `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.ParseOptions` -> `SchemaAST.ParseOptions`: Pass parsing options at runtime. onExcessProperty supports ignore or error, not preserve; model extra values with an explicit Record or StructWithRest. propertyOrder was removed. concurrency follows Effect.forEach semantics for tuple, array, struct, record, and struct-with-rest children, applies independently at each nested product, and does not make Union candidates concurrent. Output key order is unspecified, including in values passed to checks. Handle required presentation or serialization order explicitly outside the parser. -- `SchemaAST.ParseOptionsAnnotationId` -> `Schema.Annotations.Bottom["parseOptions"]`: Symbol annotation IDs were removed; use the parseOptions key. +- `SchemaAST.ParseOptionsAnnotationId` -> `none`: Parse options are no longer schema annotations. Pass options when creating or calling a decoder or encoder; there is no annotation-based override for nested schemas. - `SchemaAST.PrettyAnnotationId` -> `Schema.overrideToFormatter`: The symbol annotation was removed; attach custom formatters with Schema.overrideToFormatter. @@ -14909,7 +15093,7 @@ Schema.toFormatter(schema) - `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.TemplateLiteral` -> `SchemaAST.TemplateLiteral`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. Parts must not contain encodings, including inside unions or nested templates; use Schema.TemplateLiteralParser for transformed parts. - `SchemaAST.TemplateLiteralSpan` -> `SchemaAST.TemplateLiteral`: Template literal parts are represented directly as AST values in v4. @@ -14921,7 +15105,7 @@ Schema.toFormatter(schema) - `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.TupleType` -> `SchemaAST.Arrays`: Use Arrays(isMutable, elements, rest, annotations?, checks?, encoding?, context?, encodingChecks?). - `SchemaAST.Type` -> `SchemaAST.AST`: The tuple-element Type wrapper was removed; optionality and mutability moved to Context. @@ -14929,13 +15113,13 @@ Schema.toFormatter(schema) - `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.TypeLiteral` -> `SchemaAST.Objects`: Use Objects(propertySignatures, indexSignatures, annotations?, checks?, encoding?, context?, encodingChecks?). Output key order is unspecified. - `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.Union` -> `SchemaAST.Union`: Pass member ASTs and an optional options object: new SchemaAST.Union(types, { mode: 'oneOf' }). Read options?.mode ?? 'anyOf' instead of a direct mode field. - `SchemaAST.UniqueSymbol` -> `SchemaAST.UniqueSymbol`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. @@ -14961,13 +15145,13 @@ Schema.toFormatter(schema) - `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.getBatchingAnnotation` -> `none`: Batching annotations were removed. ParseOptions.concurrency controls product child parsing only; configure request batching separately. - `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.getConcurrencyAnnotation` -> `SchemaAST.ParseOptions`: Concurrency is no longer read from schema annotations. Pass it in runtime ParseOptions when creating or calling a parser. It applies to product children, not Union candidates. - `SchemaAST.getDecodingFallbackAnnotation` -> `none`: Fallbacks are encoding middleware in v4, not readable annotations; attach them with Schema.catchDecoding. @@ -14993,7 +15177,7 @@ Schema.toFormatter(schema) - `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.getParseOptionsAnnotation` -> `none`: Parse options are no longer schema annotations. Pass options when creating or calling a decoder or encoder; there is no annotation-based override for nested schemas. - `SchemaAST.getPropertySignatures` -> `SchemaAST.Objects.propertySignatures`: Narrow to Objects and read propertySignatures directly. @@ -15061,6 +15245,8 @@ Schema.toFormatter(schema) - `SchemaAST.partial` -> `Schema.mapFields + Struct.map(Schema.optional)`: Partial object transforms moved to schema field transforms. +- `SchemaAST.pick` -> `none`: The low-level AST picker was removed. Keep field selection at the Schema.Struct level with mapFields and Struct.pick, or discriminate and rebuild custom AST nodes explicitly. + - `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. @@ -15123,6 +15309,8 @@ Schema.toFormatter(schema) - `Secret.fromIterable` -> `Redacted.make(Array.from(iterable).join(""))`: Secret was removed; join the character iterable and wrap the resulting string in Redacted. +- `Secret.fromString`: TODO: needs guidance + - `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. @@ -15363,6 +15551,8 @@ Schema.toFormatter(schema) - `SortedSet.some` -> `HashSet.some`: Run the predicate against the replacement HashSet; sort first only if traversal order has observable effects. +- `SortedSet.toggle` -> `HashSet.has + HashSet.add / HashSet.remove`: HashSet has no toggle; branch on membership and add or remove the element. + - `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. @@ -15389,7 +15579,7 @@ Schema.toFormatter(schema) - `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.as` -> `Stream.as`: Retained in v4 with the same signature; replaces every element with the provided constant value. - `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. @@ -15399,6 +15589,10 @@ Schema.toFormatter(schema) - `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.bind` -> `Stream.bind`: The input must be a record. Start from Stream.Do or use Stream.bindTo(name) to wrap a non-record element. The field name may now re-bind an existing key; the resulting record type is Simplify\ & Record\\>. + +- `Stream.bindEffect` -> `Stream.bindEffect`: The input must be a record. Start from Stream.Do or use Stream.bindTo(name) to wrap a non-record element. Existing keys may be re-bound, with the new value replacing the old field type. The callback still returns an Effect. + #### `Stream.branchAfter` **Replacement:** `Stream.peel` @@ -15427,6 +15621,8 @@ Stream.unwrap(Effect.map(Stream.peel(self, Sink.take(n)), ([head, rest]) => f(he - `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.catchTags` -> `Stream.catchTags`: Only keys present in the tagged error union are accepted; remove handlers for tags the stream cannot fail with. Untagged errors remain in the error channel. + - `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. @@ -15501,6 +15697,10 @@ Stream.unwrap(Effect.map(Stream.peel(self, Sink.take(n)), ([head, rest]) => f(he - `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.let` -> `Stream.let`: Retained in v4 and aligned with Effect.let: the field name may re-bind an existing key (the computed value replaces it) and the record type is Simplify\ & Record\\>. + +- `Stream.mapBoth` -> `Stream.mapBoth`: Rename the options onSuccess to onElement and onFailure to onError. The callbacks still transform emitted elements and typed errors respectively. + - `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. @@ -15553,6 +15753,8 @@ Stream.mergeAll(Object.entries(streams).map(([_tag, s]) => Stream.map(s, (value) - `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.partition` -> `Stream.partition`: Pass a Result-returning Filter instead of a boolean predicate or refinement; Filter.fromPredicate(predicate) adapts existing predicates. The tuple order is now [passes, fails], the reverse of v3 [excluded, satisfying]. Rename bufferSize to capacity (default 16; also accepts "unbounded"). Acquisition is scoped and does not fail with E; the returned streams carry E. + - `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. @@ -15623,6 +15825,8 @@ Effect.suspend(() => { - `Stream.runForEachWhileScoped` -> `Stream.runForEachWhile`: Scoped run variants are gone; v4 runForEachWhile (callback returns Effect\) manages the stream scope internally. +- `Stream.runIntoPubSub` -> `Stream.runIntoPubSub`: Pass a PubSub of plain elements rather than Take wrappers. Both overloads return Effect\, so handle stream failures on the returned Effect instead of expecting them as PubSub messages. The optional shutdownOnEnd flag controls PubSub shutdown. + - `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. @@ -15631,6 +15835,10 @@ Effect.suspend(() => { - `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.scan` -> `Stream.scan`: Wrap the initial state in a thunk: Stream.scan(() =\> initial, step). The thunk is evaluated for each stream run; allocate mutable initial state inside it to avoid sharing state between runs. + +- `Stream.scanEffect` -> `Stream.scanEffect`: Wrap the initial state in a thunk: Stream.scanEffect(() =\> initial, step). It is evaluated for each stream run; the step still returns an Effect. + - `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. @@ -15828,9 +16036,9 @@ switch (strategy) { ### `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` -> `SynchronizedRef.SynchronizedRef`: The model remains but no longer extends Ref; read and update it through explicit SynchronizedRef operations. The curried v4 modifySomeEffect takes only the callback, which returns an Effect of [result, Option\]; remove the v3 fallback and outer Option. -- `SynchronizedRef.SynchronizedRef.Variance` -> `Ref.Ref.Variance`: SynchronizedRef now inherits Ref variance instead of declaring a separate public variance marker. +- `SynchronizedRef.SynchronizedRef.Variance` -> `none`: The public nested variance marker was removed. SynchronizedRef uses an internal brand in v4, so do not refer to a variance interface directly. - `SynchronizedRef.SynchronizedRefTypeId` -> `none`: The SynchronizedRef type id is internal in v4; do not inspect or construct the brand directly. @@ -15972,6 +16180,8 @@ switch (strategy) { - `TMap.removeAll` -> `TxHashMap.removeMany`: The bulk removal operation was renamed. +- `TMap.set`: TODO: needs guidance + - `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. diff --git a/repos/effect/package.json b/repos/effect/package.json index e114088fd..1be138c69 100644 --- a/repos/effect/package.json +++ b/repos/effect/package.json @@ -22,6 +22,7 @@ "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", + "check-dist-types": "node scripts/check-dist-types.mjs", "jsdocs": "effect-jsdocs", "lint": "oxlint -f unix && dprint check", "lint-fix": "oxlint --fix && dprint fmt", @@ -33,19 +34,19 @@ "changeset-publish": "node scripts/set-strip-internal.mjs && pnpm codemod && pnpm build && changeset publish" }, "devDependencies": { - "@babel/cli": "^8.0.4", - "@babel/core": "^8.0.1", + "@babel/cli": "^8.0.6", + "@babel/core": "^8.0.6", "@babel/plugin-transform-export-namespace-from": "^8.0.1", "@babel/plugin-transform-modules-commonjs": "^8.0.1", - "@changesets/changelog-github": "1.0.0", - "@changesets/cli": "3.0.1", + "@changesets/changelog-github": "1.0.1", + "@changesets/cli": "3.0.3", "@effect/ai-docgen": "workspace:^", "@effect/bundle": "workspace:^", "@effect/docgen": "workspace:^", "@effect/doctest": "workspace:^", "@effect/jsdocs": "workspace:^", "@effect/oxc": "workspace:^", - "@effect/tsgo": "^0.38.0", + "@effect/tsgo": "^0.45.0", "@effect/utils": "workspace:^", "@effect/vitest": "workspace:^", "@faker-js/faker": "^10.6.0", @@ -53,33 +54,31 @@ "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@types/jscodeshift": "^17.3.0", - "@types/node": "^26.4.0", - "@vitest/browser": "^4.1.11", - "@vitest/coverage-v8": "^4.1.11", - "@vitest/expect": "^4.1.11", - "@vitest/web-worker": "^4.1.11", - "ast-types": "^0.16.1", + "@types/node": "^26.6.2", + "@vitest/coverage-v8": "^5.0.1", + "@vitest/web-worker": "^5.0.1", + "ast-types": "^0.16.3", "babel-plugin-annotate-pure-calls": "^0.5.0", - "dprint": "^0.56.1", - "fast-check": "^4.9.0", + "dprint": "^0.57.4", + "fast-check": "^4.10.2", "glob": "^13.0.6", - "happy-dom": "^20.12.0", + "happy-dom": "^20.14.5", "jscodeshift": "^17.4.0", "lalph": "^0.3.139", "madge": "^8.0.0", - "oxlint": "^1.80.0", + "oxlint": "^1.83.0", "pkg-pr-new": "0.0.88", - "playwright": "^1.62.1", - "rollup": "^4.63.1", + "playwright": "^1.63.0", + "rollup": "^4.63.4", "rollup-plugin-bundle-stats": "^4.22.3", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-visualizer": "^7.1.1", "terser": "^5.51.2", - "tstyche": "^7.2.3", + "tstyche": "^7.2.5", "typescript": "^7.0.2", - "vite": "^8.2.2", - "vitest": "^4.1.11", - "vitest-websocket-mock": "^0.7.0", - "zod": "^4.5.4" + "vite": "^8.3.0", + "vitest": "^5.0.1", + "vitest-websocket-mock": "^0.8.0", + "zod": "^4.6.5" } } diff --git a/repos/effect/packages/ai/anthropic/CHANGELOG.md b/repos/effect/packages/ai/anthropic/CHANGELOG.md index d1b848563..5750d3726 100644 --- a/repos/effect/packages/ai/anthropic/CHANGELOG.md +++ b/repos/effect/packages/ai/anthropic/CHANGELOG.md @@ -1,5 +1,49 @@ # @effect/ai-anthropic +## 4.0.0-rc.117 + +### Patch Changes + +- Updated dependencies [[`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9), [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724), [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0), [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b), [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9), [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a), [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5), [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741)]: + - effect@4.0.0-rc.117 + +## 4.0.0-rc.116 + +### Patch Changes + +- Updated dependencies [[`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f), [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2), [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d), [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356), [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf), [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b), [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f), [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a), [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac), [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06), [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8), [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153), [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537), [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746), [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9), [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c), [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3), [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b), [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f), [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5), [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55), [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b)]: + - effect@4.0.0-rc.116 + +## 4.0.0-rc.115 + +### Patch Changes + +- Updated dependencies [[`657254b`](https://github.com/Effect-TS/effect/commit/657254b8218628b0116497d09aaf783cb88279f3), [`f9ef0e9`](https://github.com/Effect-TS/effect/commit/f9ef0e970260b5de143392d00593e6c8b4614cff), [`4f73f9e`](https://github.com/Effect-TS/effect/commit/4f73f9e0bd9538260f0f7166e0e6abdbd507a0e8)]: + - effect@4.0.0-rc.115 + +## 4.0.0-rc.114 + +### Patch Changes + +- Updated dependencies [[`3ff4952`](https://github.com/Effect-TS/effect/commit/3ff49521afa6fe6c85c2c230f7e986f7374ef353), [`6d55555`](https://github.com/Effect-TS/effect/commit/6d55555b83701c42855758944985a6fdbf57fffe), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`d4e4ad5`](https://github.com/Effect-TS/effect/commit/d4e4ad57d06d0a30d5e8090ed951f080bcd3a3d7), [`b1988f4`](https://github.com/Effect-TS/effect/commit/b1988f496eef07482f10f4cc03ef20b9570ea0ac), [`482b7d7`](https://github.com/Effect-TS/effect/commit/482b7d7eb08ebe6bc57781414a5a9a12600b2c71), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`9941e6d`](https://github.com/Effect-TS/effect/commit/9941e6dbf800cfd3723bfb421306e04e36b5882d)]: + - effect@4.0.0-rc.114 + +## 4.0.0-rc.113 + +### Patch Changes + +- [#7758](https://github.com/Effect-TS/effect/pull/7758) [`8479f4e`](https://github.com/Effect-TS/effect/commit/8479f4e91f67209106ad34af0b665ebce2b15f1c) Thanks @kitlangton! - Preserve base64 image strings in Anthropic requests. + +- [#8098](https://github.com/Effect-TS/effect/pull/8098) [`ad8e9ad`](https://github.com/Effect-TS/effect/commit/ad8e9ad7aff4a4ff50886db2c48b4d136c3d5980) Thanks @tim-smart! - Exclude the provider-only `strictJsonSchema` option from Anthropic Messages request bodies while preserving its control over tool strictness. + +- [#8100](https://github.com/Effect-TS/effect/pull/8100) [`8f3ea0d`](https://github.com/Effect-TS/effect/commit/8f3ea0d0054cbf9f7700e82e0801b8fe4c511bc1) Thanks @tim-smart! - Fix non-native structured output generation by always requesting the response tool and excluding accompanying prose when decoding its JSON payload. + +- [#7588](https://github.com/Effect-TS/effect/pull/7588) [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494) Thanks @tim-smart! - Route tool call parameter validation failures through the tool's `failureMode` and drop `ToolParameterValidationError.toolParams`. + +- [#7805](https://github.com/Effect-TS/effect/pull/7805) [`0b93584`](https://github.com/Effect-TS/effect/commit/0b935848082dd178db8278bff52a2cd07e5bf69a) Thanks @tim-smart! - Preserve plain-text client tool results when encoding provider requests. +- Updated dependencies [[`49e3901`](https://github.com/Effect-TS/effect/commit/49e39010ff84acbde32c50d559a2f76426cf830e), [`b945ded`](https://github.com/Effect-TS/effect/commit/b945ded23aa9a0ad88bb55aa4089680866dccf92), [`d6422f4`](https://github.com/Effect-TS/effect/commit/d6422f4104062e3e176abe92ef0e444cd4cc6bab), [`5a80204`](https://github.com/Effect-TS/effect/commit/5a802043984727b0c5a291af39d1b9bbfa8d7b8b), [`53511ef`](https://github.com/Effect-TS/effect/commit/53511efcd1c7a05d34b4c9a19c19ed317ebc5d4c), [`79ae49f`](https://github.com/Effect-TS/effect/commit/79ae49f19cab9a42c8f27ae8b104d5e39e6131fa), [`0d083ba`](https://github.com/Effect-TS/effect/commit/0d083ba26b2e1afec8d3e8d83db0d05683b6602b), [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803), [`debe8fd`](https://github.com/Effect-TS/effect/commit/debe8fdc15d00d51cf707353365d7b877123eec6), [`a8588f9`](https://github.com/Effect-TS/effect/commit/a8588f99a43dc9b85dacb14f7be5d3e1ed994d4f), [`f17eb0a`](https://github.com/Effect-TS/effect/commit/f17eb0a6f669e09647c5ded5832ea6baac596659), [`f30cbfe`](https://github.com/Effect-TS/effect/commit/f30cbfe53951128ad8124ae733f12423dbc78bd8), [`78cc9c0`](https://github.com/Effect-TS/effect/commit/78cc9c0d0f36b59f7034b2ace56a4a5c02b7a551), [`ccbdbd5`](https://github.com/Effect-TS/effect/commit/ccbdbd51e7bdb114302701fbc03021868b71956a), [`a63dcbf`](https://github.com/Effect-TS/effect/commit/a63dcbf04e5c3d8d934a41bc6122e9951b1cefa9), [`b845b18`](https://github.com/Effect-TS/effect/commit/b845b186379106c17ed4b1149e2462cd0d2d77b5), [`381b794`](https://github.com/Effect-TS/effect/commit/381b79464f8e0a520585fdcfdd3e95e0553d0b27), [`4ffcaf4`](https://github.com/Effect-TS/effect/commit/4ffcaf4c4a94d3d9d5715db91aa37d7f5d9ecd99), [`ba2fd82`](https://github.com/Effect-TS/effect/commit/ba2fd822e9618f30d0f430ae4eed2eaa4813e2e3), [`02be94c`](https://github.com/Effect-TS/effect/commit/02be94ce78a3c9fc1e4c516d37d8e11e1c0eef2e), [`115d8c2`](https://github.com/Effect-TS/effect/commit/115d8c22599640ece2fd6a10564925b1d79f8a8c), [`1452635`](https://github.com/Effect-TS/effect/commit/14526354f3d0288bd1d0a85c8d4aeee8abe2cd9b), [`77f85fe`](https://github.com/Effect-TS/effect/commit/77f85fe1613348f5c990016b49dc97e252576c82), [`a3f2b31`](https://github.com/Effect-TS/effect/commit/a3f2b319343f28edb028270f7074066cea0ec429), [`310f8d3`](https://github.com/Effect-TS/effect/commit/310f8d3424a025ce1f1cc5c338232db2362052d8), [`291d616`](https://github.com/Effect-TS/effect/commit/291d616282b666bbba3664143fa1d19e839a6a5d), [`48dbbb2`](https://github.com/Effect-TS/effect/commit/48dbbb22bb2e04f63992fc267dd711a0cbd76d72), [`b43bfd6`](https://github.com/Effect-TS/effect/commit/b43bfd644a3205f1518abbfd006f761e31b3a5f9), [`1c89c78`](https://github.com/Effect-TS/effect/commit/1c89c78f9faa3638f398772f3ed7182aa97a1edd), [`9bbe1a5`](https://github.com/Effect-TS/effect/commit/9bbe1a5940eee4642ffe854356128b617e0a064c), [`dd99ab0`](https://github.com/Effect-TS/effect/commit/dd99ab007e3352761187dae330d52f65feeff7c0), [`8f397ed`](https://github.com/Effect-TS/effect/commit/8f397ed4e7852026591819cc4c8c1995bdd2fb8c), [`d7ae6b6`](https://github.com/Effect-TS/effect/commit/d7ae6b6491a88f2710612bfcddaf608ebe925f7c), [`8cf1203`](https://github.com/Effect-TS/effect/commit/8cf12030e8a3e1e76384884d41afa96e2bd72b13), [`87654c5`](https://github.com/Effect-TS/effect/commit/87654c5eed150387f4eac7153751b6085616ba52), [`f05ae0b`](https://github.com/Effect-TS/effect/commit/f05ae0b3fd2cfc7f12b668af244b3cf3fae59bc5), [`436f5eb`](https://github.com/Effect-TS/effect/commit/436f5eb44aa234bdf1de37b07da2ce8dc31ad4ec), [`d8ff960`](https://github.com/Effect-TS/effect/commit/d8ff9605bb5ef7b0cacd80e3b43ed1ce9e3d5e36), [`8766475`](https://github.com/Effect-TS/effect/commit/876647502300b9fb79d163de10fd497716d93be0), [`b64f406`](https://github.com/Effect-TS/effect/commit/b64f406de13a1b9da4664dbde2dbddaa5f776a05), [`4697aaa`](https://github.com/Effect-TS/effect/commit/4697aaa1d38f9da7fe0bcb62f708183f766fe496), [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494), [`9956f0e`](https://github.com/Effect-TS/effect/commit/9956f0e6f47096a6e31305ec2c7320b8e1a140af), [`5c7eed0`](https://github.com/Effect-TS/effect/commit/5c7eed0c5aa0988716206dd374359223edf828ee), [`183c2ea`](https://github.com/Effect-TS/effect/commit/183c2ea65ee1e9b967c746aa1b0bf2b8d3422de2), [`1e92dbd`](https://github.com/Effect-TS/effect/commit/1e92dbd26f5a74bb32103cbaac0f09460b7954de), [`4eb0fa7`](https://github.com/Effect-TS/effect/commit/4eb0fa72302b0a63ac34c6515a9cdf96338e586a), [`534b8b9`](https://github.com/Effect-TS/effect/commit/534b8b9dba195ec38a4795fe564d5e0876cb6468), [`b76a1cf`](https://github.com/Effect-TS/effect/commit/b76a1cf32b0a742c24c1874137e2da8fed5c48eb), [`acc1e53`](https://github.com/Effect-TS/effect/commit/acc1e5384aa5cb7b072f60308013f078b0099bdf), [`4950a91`](https://github.com/Effect-TS/effect/commit/4950a914a68306531a1111d4cdc4504aa8d3854e), [`c020987`](https://github.com/Effect-TS/effect/commit/c020987a0611c9e610a7eef347648b60026745b8), [`abe95d1`](https://github.com/Effect-TS/effect/commit/abe95d12340e1dd435e5df623b1967bf01938c94), [`027ceb9`](https://github.com/Effect-TS/effect/commit/027ceb91e003fbc36f0559f72e1c614cd9198c1a), [`3d203b7`](https://github.com/Effect-TS/effect/commit/3d203b702e26687d2fea4aaae7cc01dcfb82c05e), [`a29b8f4`](https://github.com/Effect-TS/effect/commit/a29b8f4de5aee58dd9ee645365c04cfe1ea6fa9f), [`ce4aa65`](https://github.com/Effect-TS/effect/commit/ce4aa65409e8500908fb6496eca06270ef5b636f), [`a8ea807`](https://github.com/Effect-TS/effect/commit/a8ea80799a0bf187c577e052fef0b7245494c7bf), [`a3ebb7f`](https://github.com/Effect-TS/effect/commit/a3ebb7fcb208f3933a40a0fd949dbcfdf1ca2921), [`473bd81`](https://github.com/Effect-TS/effect/commit/473bd814a97dba66070435cfe84933bcc143effe), [`53843f6`](https://github.com/Effect-TS/effect/commit/53843f6490f4eebaf1eeb91fdcc5f1c542b0e132), [`c5eca65`](https://github.com/Effect-TS/effect/commit/c5eca6539f0d605acd5ce19437fa88f167d69bb7), [`d6f9eba`](https://github.com/Effect-TS/effect/commit/d6f9eba7e8df0c8bd74762c313402884d94ba146), [`8d1e97a`](https://github.com/Effect-TS/effect/commit/8d1e97adbf5a36b4b53ab56f797c2ec0267a8821), [`b28ab48`](https://github.com/Effect-TS/effect/commit/b28ab48be7cf7f2bd30f9467ccac0ab41260f7ad), [`9960708`](https://github.com/Effect-TS/effect/commit/9960708052fcaaa873c471aea144ca46955e6023), [`8ac53b6`](https://github.com/Effect-TS/effect/commit/8ac53b6ae677f06d88d7d81f932ab5ff9b59a10e), [`84ad49a`](https://github.com/Effect-TS/effect/commit/84ad49a357ef156b4ff46cefc945efbe3e292eff), [`72cfa24`](https://github.com/Effect-TS/effect/commit/72cfa2420977919ba57a8e6de538e01e06d5f380), [`95c2581`](https://github.com/Effect-TS/effect/commit/95c2581e0f78e1537aad21764fe562206ca50adb), [`d5c7cd2`](https://github.com/Effect-TS/effect/commit/d5c7cd2c47b2b01523aaea48d2dd039b89b36114), [`fe4fed1`](https://github.com/Effect-TS/effect/commit/fe4fed1b014c78043a8efe37520c527100910010), [`d150a64`](https://github.com/Effect-TS/effect/commit/d150a643c4f60da2dc19ae813b807d48b580087e), [`05b1e80`](https://github.com/Effect-TS/effect/commit/05b1e80cde3a8da1ff1afe288f8539b72c05bb87), [`414dc90`](https://github.com/Effect-TS/effect/commit/414dc9056a39aa22f7cc27aeb5e631231ce50982), [`3f51acd`](https://github.com/Effect-TS/effect/commit/3f51acdf5e2490d7d0a833cd67ce06d1d867facd), [`d68ff05`](https://github.com/Effect-TS/effect/commit/d68ff05506ab5bb3ec07e1b759ce296423b0dbbe), [`f3cf1e6`](https://github.com/Effect-TS/effect/commit/f3cf1e6c36f1aac8399d9b540aecb6410be3096f), [`6525771`](https://github.com/Effect-TS/effect/commit/6525771eef7df6b71a3ef6e299ee93d798ed3dda), [`4ab4e83`](https://github.com/Effect-TS/effect/commit/4ab4e838a4c9ab3c9cc0d52d3e83de88686601b0), [`f7f1d78`](https://github.com/Effect-TS/effect/commit/f7f1d78e52023bdfbebf76949c550db8bb9411dd), [`47b358a`](https://github.com/Effect-TS/effect/commit/47b358a4b85d22973a7957abd1a859f65b75b7da), [`45ffa72`](https://github.com/Effect-TS/effect/commit/45ffa7221259c7e4be9797b90f6ca569fe7ec8a3), [`6232650`](https://github.com/Effect-TS/effect/commit/62326505a74fcd8fe27c8e2064128d2dea484ecc), [`4b73e1b`](https://github.com/Effect-TS/effect/commit/4b73e1bd5576346d7939be8915f3053e428d1c0e), [`284050c`](https://github.com/Effect-TS/effect/commit/284050c2aa6e78eceb08e5eff1803501cc80a658), [`c85fc0b`](https://github.com/Effect-TS/effect/commit/c85fc0b8f2195780929d6fc8ecdc084faf18736c), [`7999b07`](https://github.com/Effect-TS/effect/commit/7999b078950fda15e235db1e61e8db5eb0632528), [`7d455f5`](https://github.com/Effect-TS/effect/commit/7d455f51acf2f0e9f710f298e14e4d81179a9259), [`d681c2e`](https://github.com/Effect-TS/effect/commit/d681c2eb1bb07d782540286368e8b42533df3063), [`84d2a47`](https://github.com/Effect-TS/effect/commit/84d2a470ecc0fcad7f1d1e7a50464e02af0e6972), [`ed74b18`](https://github.com/Effect-TS/effect/commit/ed74b18b95b0797808f2f7928f413f1cf2279d77), [`2245997`](https://github.com/Effect-TS/effect/commit/2245997ad2214591b7d9674777595092926743cf), [`d386979`](https://github.com/Effect-TS/effect/commit/d386979fedbdd433d28816f9cc74675a9af45189), [`fc9fedf`](https://github.com/Effect-TS/effect/commit/fc9fedf59fc26a8bc6e45c3860a921f3b7e0f971), [`7750dbe`](https://github.com/Effect-TS/effect/commit/7750dbe0d5b9929c6d92b0365b42784b993ea2a6), [`fc91af6`](https://github.com/Effect-TS/effect/commit/fc91af659d0682659e6347eede288f90f618acc0), [`39b9738`](https://github.com/Effect-TS/effect/commit/39b9738727ab4d87298fd640edd138f352c70ee3), [`88093b5`](https://github.com/Effect-TS/effect/commit/88093b57951a1b597599fe4772ab690b36a95818), [`d14c463`](https://github.com/Effect-TS/effect/commit/d14c4631ac82911a8ca0b6c24c14ecb95863ee6b), [`d473bd3`](https://github.com/Effect-TS/effect/commit/d473bd3ee966a3a0c245d0e3c752fd91368b8ff6), [`84864bc`](https://github.com/Effect-TS/effect/commit/84864bc30c9e92a1226f65bb78b0641a7e0acea2), [`e2ae724`](https://github.com/Effect-TS/effect/commit/e2ae72481e755a454f28ce45e90f43b857b7c6e5), [`f921ed3`](https://github.com/Effect-TS/effect/commit/f921ed305c63415dc468e94d9319cc3237119138), [`1df933d`](https://github.com/Effect-TS/effect/commit/1df933d1dee2e09a25f9c27214b31aa94fa9d020), [`829aff9`](https://github.com/Effect-TS/effect/commit/829aff97d71d7900b7765ba2f63e33874b898cdc), [`aa0aba3`](https://github.com/Effect-TS/effect/commit/aa0aba383382ca1b99fbe618dceba63c3a5a6a53), [`a71140f`](https://github.com/Effect-TS/effect/commit/a71140fbe79809aa5311a55b1c6908905dc2df26), [`0276a27`](https://github.com/Effect-TS/effect/commit/0276a27ddbb15d163daf56f059ca9f62c4fd9939), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`c86c999`](https://github.com/Effect-TS/effect/commit/c86c99939e1628813a2f92f0906cfbbe924504e8), [`975f758`](https://github.com/Effect-TS/effect/commit/975f7586f3dcda5743d6570a740013a9fbdce9a7), [`2a3a478`](https://github.com/Effect-TS/effect/commit/2a3a4783bcd2d4e91e3505c7a6c1e398fea89595), [`1e6e206`](https://github.com/Effect-TS/effect/commit/1e6e206f6327a114f55a541775f465c0bc9fbf3e), [`fa6027b`](https://github.com/Effect-TS/effect/commit/fa6027b16e65376078e526fa33116ac0cc82485b), [`7616f73`](https://github.com/Effect-TS/effect/commit/7616f73c16bd45a3a3e2202d3f81c6d1cc63f4e0), [`fc668b6`](https://github.com/Effect-TS/effect/commit/fc668b61e9755545a47582dee6c4425d5652cbb6), [`d425c8c`](https://github.com/Effect-TS/effect/commit/d425c8c1319519e861714bcb9d055b63a9434fad), [`ce120f4`](https://github.com/Effect-TS/effect/commit/ce120f474b87d8a5a787dd7f848177ecd3b960fc), [`248201f`](https://github.com/Effect-TS/effect/commit/248201f7846d855685a4dc1b2f01968981706d69), [`e80d397`](https://github.com/Effect-TS/effect/commit/e80d3972fcca4e986b6497d67406f45ee1be540d), [`f1a941d`](https://github.com/Effect-TS/effect/commit/f1a941d52c2f71973e998883869e4e9ef510abf2), [`73bc3a1`](https://github.com/Effect-TS/effect/commit/73bc3a13b6e1b7a060e6bcd19ae7796443211a80), [`b628bb1`](https://github.com/Effect-TS/effect/commit/b628bb1240a09df2244c3ff6c587f978feb25225), [`6e3ae7b`](https://github.com/Effect-TS/effect/commit/6e3ae7b6359bda08cea74b761fd54224d7af45e2), [`e891247`](https://github.com/Effect-TS/effect/commit/e8912473bd4128714a0f8cb04621f3e12f62a910), [`53e6c73`](https://github.com/Effect-TS/effect/commit/53e6c7311b948a62e93e57f6daee66a8ec24b030), [`1579d6f`](https://github.com/Effect-TS/effect/commit/1579d6fd063e444e4fcdfd08ff194c7485203f9c), [`d3c6b73`](https://github.com/Effect-TS/effect/commit/d3c6b73cc4b78f22fdb2e25c8bebda027551540b), [`4a59c6a`](https://github.com/Effect-TS/effect/commit/4a59c6a559e69d399b00c6acd48c8e1977e34875), [`145d8e1`](https://github.com/Effect-TS/effect/commit/145d8e1013220425b8edf34f7011c73f73e1cdcf), [`f74282c`](https://github.com/Effect-TS/effect/commit/f74282cb08de808b678ce67687acbcd61939d2f6), [`0a38623`](https://github.com/Effect-TS/effect/commit/0a3862349ebfbc41746aa0de5e682992719c1da6), [`0a08ae0`](https://github.com/Effect-TS/effect/commit/0a08ae0626f8124779ec2e32ae2088d28db157c6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`d60c5d4`](https://github.com/Effect-TS/effect/commit/d60c5d40b5954ea1557aef1502b4b87f98bbf134), [`07ffd25`](https://github.com/Effect-TS/effect/commit/07ffd25bd0e82c9e6b72b8e669833fb4531a9da3), [`9b517ad`](https://github.com/Effect-TS/effect/commit/9b517ad28a0c2f213693bcab408658c40e3b6d9b), [`604b1c1`](https://github.com/Effect-TS/effect/commit/604b1c109d6c9e654b9b7fcc98d6b80361400325), [`ccc2e02`](https://github.com/Effect-TS/effect/commit/ccc2e02def5965f5535ca97fac2051f4387c4e13), [`14d810a`](https://github.com/Effect-TS/effect/commit/14d810a726caad8a7b0d42f04a6876f25fbcc005), [`a9d1ee3`](https://github.com/Effect-TS/effect/commit/a9d1ee3d4d51e97ea33440fe6100935d5fd5aada), [`a31adbe`](https://github.com/Effect-TS/effect/commit/a31adbefd6c04c70536de11433edba9edd1f5013), [`7245f87`](https://github.com/Effect-TS/effect/commit/7245f87ace09806d15893df8f0bac8f0b9b817db), [`3a0828b`](https://github.com/Effect-TS/effect/commit/3a0828b058abb4b4646bd880458b4e636d77d1c3), [`cd83544`](https://github.com/Effect-TS/effect/commit/cd83544dd594b99e8ea0d7a7645c086f5861769b), [`6550a07`](https://github.com/Effect-TS/effect/commit/6550a074c5c501d8a95dcd354f17e9785a1ae1eb), [`6f090d4`](https://github.com/Effect-TS/effect/commit/6f090d4c2032cc35f7f9a9eac900d0329b8ac07e), [`b505c0d`](https://github.com/Effect-TS/effect/commit/b505c0db03e86eb126237690a5867f457eb4237f), [`186dd49`](https://github.com/Effect-TS/effect/commit/186dd4914084fa346a870ce9c637b9c2a6cc8100), [`9f37e58`](https://github.com/Effect-TS/effect/commit/9f37e58d00e06150d96af48fffe780947ca0d519), [`4446451`](https://github.com/Effect-TS/effect/commit/44464510114773e1ebd1abb6efffa382cf69998e), [`58be972`](https://github.com/Effect-TS/effect/commit/58be97238ee1f6cd26f6a3a645d1df97a091a224), [`1320075`](https://github.com/Effect-TS/effect/commit/1320075fd4ed381f60f6f37f280dbec17cb29b81), [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80), [`f7490d4`](https://github.com/Effect-TS/effect/commit/f7490d49c75b99118c6c42549511ffa166459f59), [`c8ea602`](https://github.com/Effect-TS/effect/commit/c8ea60231ad69e23c0bf319f6f6b3ff28fc45e4b), [`62d82f4`](https://github.com/Effect-TS/effect/commit/62d82f4919c72d8cc0cfd2b704ed6900f69ad9a8), [`a2c9e7c`](https://github.com/Effect-TS/effect/commit/a2c9e7c17a4118b74a1eab3332042dafe0bb7318), [`97dd022`](https://github.com/Effect-TS/effect/commit/97dd022fe7fbd1696dd60f3319eae594abdd3760), [`f984ee8`](https://github.com/Effect-TS/effect/commit/f984ee82d255c7958eeed6f0dd08de1b88cbbfd6), [`f1b2910`](https://github.com/Effect-TS/effect/commit/f1b291027c3055f801c2f288dda21ba84c361956), [`a29e05a`](https://github.com/Effect-TS/effect/commit/a29e05a907053a8617763b4e7408e1f012b59049), [`1aa1d8b`](https://github.com/Effect-TS/effect/commit/1aa1d8bd0f2c2d429938657927f34f8ff0f940a0), [`bb99734`](https://github.com/Effect-TS/effect/commit/bb99734ef87bcc377c628bc4072aa856ce5f7ca6), [`222e7ca`](https://github.com/Effect-TS/effect/commit/222e7caa4bb0da6d81a4eabf25dffc181c20f6a3), [`797c9e3`](https://github.com/Effect-TS/effect/commit/797c9e3c1f2e66db1570cf6aa5c4a15e94507bf0), [`b4d5398`](https://github.com/Effect-TS/effect/commit/b4d5398598c04a84054a55873c943d587880058d), [`c8349ed`](https://github.com/Effect-TS/effect/commit/c8349ede1a2add807e9ec9c37612b3a39bf284f8), [`8426e5f`](https://github.com/Effect-TS/effect/commit/8426e5f7ad11f15e39565cf8ba0874bf974f5085), [`26e0085`](https://github.com/Effect-TS/effect/commit/26e0085d098f6e526c085c357b8d82ee068b6518), [`1a86166`](https://github.com/Effect-TS/effect/commit/1a86166f102b517ee4d89dca0095e28f738eee10), [`0856631`](https://github.com/Effect-TS/effect/commit/0856631a89d8f72d206c366905ddac75468a8e85), [`0af0985`](https://github.com/Effect-TS/effect/commit/0af0985d5c132b33e14a2df59e0567d7ba2a1710), [`bc582c9`](https://github.com/Effect-TS/effect/commit/bc582c94313cbb89d87b3844ffffabebbb4ffa83), [`2c63f1e`](https://github.com/Effect-TS/effect/commit/2c63f1e07e375b7958e14929c6a4b358c819a9d0), [`0d98213`](https://github.com/Effect-TS/effect/commit/0d9821346453d44696258103643d5482e63638b4), [`ca6f0dc`](https://github.com/Effect-TS/effect/commit/ca6f0dc4fcea564b351bf505df015d16a8b965ed), [`d8cc9ed`](https://github.com/Effect-TS/effect/commit/d8cc9ed80e1df9c056a46ba62df01f7fc0f531c3), [`42fd969`](https://github.com/Effect-TS/effect/commit/42fd969b45025eed6d60cb87b97c5ff88d187671), [`5641ad3`](https://github.com/Effect-TS/effect/commit/5641ad333a88bb9e56baf886006682678391be0a), [`629870d`](https://github.com/Effect-TS/effect/commit/629870d461b36d3dacb30c8d203b8d20d4beaba0), [`d592c14`](https://github.com/Effect-TS/effect/commit/d592c1492ad895ea72537e10387b735f3ba0e7de), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`53909a9`](https://github.com/Effect-TS/effect/commit/53909a9bf14b29725a08dcccde02bee8e95a6b14), [`78a4269`](https://github.com/Effect-TS/effect/commit/78a42697f6085c87c09b18e8095bb7541bd65261), [`06c6307`](https://github.com/Effect-TS/effect/commit/06c6307d3d5538bc398c67a44239bc3d52ab1a4d), [`0847c41`](https://github.com/Effect-TS/effect/commit/0847c41e2472a347cd8f77d460d3cbdfecd4adf0), [`5a77084`](https://github.com/Effect-TS/effect/commit/5a77084651bc58617b9988606473cf9ba5ca0032), [`96f99b3`](https://github.com/Effect-TS/effect/commit/96f99b325da32bfa535e826d89d838d0edec0dea), [`7bb8781`](https://github.com/Effect-TS/effect/commit/7bb878176b8ad20cf0e671b570caf72e194f9515), [`4907e9b`](https://github.com/Effect-TS/effect/commit/4907e9b2470894ddaf0cbdec2612b59f04a51dbe), [`2a30248`](https://github.com/Effect-TS/effect/commit/2a30248b6eb739f22403456209bc468f2f4ef26a), [`8364ddd`](https://github.com/Effect-TS/effect/commit/8364ddda233d35ee472cc2356312317a60a148a2), [`ad67d8c`](https://github.com/Effect-TS/effect/commit/ad67d8c5a1b195ff5468a9f2e963c59aa0b3166f), [`ec0c087`](https://github.com/Effect-TS/effect/commit/ec0c0875262fb81c7992f5023776b8fa894c49b6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`a2c1ce6`](https://github.com/Effect-TS/effect/commit/a2c1ce62f04214d98d848f4cfe33d677e1a87d39), [`91e9af0`](https://github.com/Effect-TS/effect/commit/91e9af0d5f138933d3a131ca1cfbc52d3bc55481), [`7bd3f34`](https://github.com/Effect-TS/effect/commit/7bd3f34385a06157b249a351e32ffdcbe1cf2c37), [`ef16581`](https://github.com/Effect-TS/effect/commit/ef165814eae0a199fdca5b3c2839bb4fcdfd5b5d), [`1693a87`](https://github.com/Effect-TS/effect/commit/1693a877324273f5a23fa7d7fc892f4ee12f20c2), [`df3fc47`](https://github.com/Effect-TS/effect/commit/df3fc47609eda7843dae2b557c8fe834299e26b4), [`e11be41`](https://github.com/Effect-TS/effect/commit/e11be41dfc73fa6b5643ab9af7df18d6907927d7), [`2e39e8b`](https://github.com/Effect-TS/effect/commit/2e39e8bafef7d3fee9f3b324b7d50cc064376667), [`11c5ee7`](https://github.com/Effect-TS/effect/commit/11c5ee7202acd3bb789d0a8ce78e6c4523071553), [`1742d2f`](https://github.com/Effect-TS/effect/commit/1742d2f48441cb7b1a8d9aca3a8e6f0acbc35a96), [`8efc70e`](https://github.com/Effect-TS/effect/commit/8efc70ea16bee43fd9adde2871cf82a8131b80a0), [`9642776`](https://github.com/Effect-TS/effect/commit/964277661423f398f777ec2eab4b3ecc0b53f53b), [`6680828`](https://github.com/Effect-TS/effect/commit/668082885f52672e4c7fad4e0fe2bfa0e69703e2), [`c34edcb`](https://github.com/Effect-TS/effect/commit/c34edcb6d7a6151d53e19828c92abf7dd8450c2a), [`7b2c5bd`](https://github.com/Effect-TS/effect/commit/7b2c5bd3c394fa4751fb229394ac9c03e401f441), [`1a2ccee`](https://github.com/Effect-TS/effect/commit/1a2ccee2bbb93514dc66e0a9cdeb52a82172ab0e), [`db995df`](https://github.com/Effect-TS/effect/commit/db995df19b0381b64cc39b50aa28d7bd8f2c1294), [`af0ccdd`](https://github.com/Effect-TS/effect/commit/af0ccdd13aa8917bce7161b8bc257fbe51c6a6a5), [`addeaea`](https://github.com/Effect-TS/effect/commit/addeaea0b0abe0dc24ca1d4d0fd194a0e458be70), [`7704034`](https://github.com/Effect-TS/effect/commit/770403411bcd8befef0552a0e40066abc417f6ea), [`e72b12f`](https://github.com/Effect-TS/effect/commit/e72b12fc305710550bc6dcb978e92de8abff88cd), [`310dd9c`](https://github.com/Effect-TS/effect/commit/310dd9ce9681e97f558bb042adb35f6040ab81e3), [`1c2afc1`](https://github.com/Effect-TS/effect/commit/1c2afc15bbf4ce5a91d24da99bc275b5bd744e77), [`44f44ca`](https://github.com/Effect-TS/effect/commit/44f44cae65249801c082c3a3848016eeeac5eb24), [`f43b9d6`](https://github.com/Effect-TS/effect/commit/f43b9d6d99cb591c0377bb1474121827201627c9), [`56e72b3`](https://github.com/Effect-TS/effect/commit/56e72b38c01bbe6ec921ad151f83b7849444f219), [`50ef80e`](https://github.com/Effect-TS/effect/commit/50ef80e251b85b5ee63aed06fe0244b5433ec2fa), [`fc3b718`](https://github.com/Effect-TS/effect/commit/fc3b7187114a31cbb21a250f55fda601bb3edcf0), [`ee336d8`](https://github.com/Effect-TS/effect/commit/ee336d80b4024b51654e7a2b8d869d244153fcdf), [`d12f922`](https://github.com/Effect-TS/effect/commit/d12f922853eff2310dbe9fc9b31562ad81b38435), [`46d8310`](https://github.com/Effect-TS/effect/commit/46d83101e8408c0428d9a8760e06e23a69ad6070), [`59812fd`](https://github.com/Effect-TS/effect/commit/59812fd221cde16fc77c03d326c6a2cf8fbec6b9), [`f9d0dec`](https://github.com/Effect-TS/effect/commit/f9d0dec12c5f229f05d7a74d532a85c6bbffefbf), [`4372c79`](https://github.com/Effect-TS/effect/commit/4372c79a32fcecc7b6b284d49a215b2c4fef9d77), [`81485ef`](https://github.com/Effect-TS/effect/commit/81485ef0288a3321e75d7d4ae32b7e0e06b6f86b), [`bd393d6`](https://github.com/Effect-TS/effect/commit/bd393d63c19bdd0ab212d95576cec89051c8501c), [`0c95c04`](https://github.com/Effect-TS/effect/commit/0c95c04b41c2d5d09e4564fd997572207fd72595)]: + - effect@4.0.0-rc.113 + ## 4.0.0-rc.112 ### Patch Changes diff --git a/repos/effect/packages/ai/anthropic/package.json b/repos/effect/packages/ai/anthropic/package.json index 6e60982cb..15d80fa28 100644 --- a/repos/effect/packages/ai/anthropic/package.json +++ b/repos/effect/packages/ai/anthropic/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-anthropic", - "version": "4.0.0-rc.112", + "version": "4.0.0-rc.117", "type": "module", "license": "MIT", "description": "An Anthropic provider integration for Effect AI SDK", diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts index 7ba02b588..52b4ddf59 100644 --- a/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts +++ b/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -20,7 +20,6 @@ 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" @@ -40,8 +39,6 @@ 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. * @@ -663,7 +660,7 @@ export const model = ( * * **When to use** * - * Use when you need to construct a `LanguageModel.Service` value backed by + * Use when you need to construct a `LanguageModel` value backed by * `AnthropicClient` inside an Effect. * * **Details** @@ -681,7 +678,7 @@ export const model = ( export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { readonly model: (string & {}) | Model readonly config?: Omit | undefined -}): Effect.fn.Return { +}): Effect.fn.Return { const client = yield* AnthropicClient const makeConfig: Effect.Effect = Effect.contextWith((services) => @@ -713,8 +710,13 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig if (betas.size > 0) { params["anthropic-beta"] = Array.from(betas).join(",") } - const { disableParallelToolCalls: _, output_config, structuredOutputs: _structuredOutputs, ...requestConfig } = - config + const { + disableParallelToolCalls: _, + output_config, + strictJsonSchema: _strictJsonSchema, + structuredOutputs: _structuredOutputs, + ...requestConfig + } = config const payload: Mutable = { ...requestConfig, max_tokens: requestConfig.max_tokens ?? capabilities.maxOutputTokens, @@ -892,7 +894,13 @@ const prepareMessages = Effect.fnUntraced( const source = isUrlData(part.data) ? { type: "url", url: getUrlString(part.data) } as const - : { type: "base64", media_type: mediaType, data: Encoding.encodeBase64(part.data) } as const + : { + type: "base64", + media_type: mediaType, + data: typeof part.data === "string" + ? part.data.replace(/^data:[^;]+;base64,/, "") + : Encoding.encodeBase64(part.data) + } as const content.push({ type: "image", source, cache_control: cacheControl }) } else if (part.mediaType === "application/pdf" || part.mediaType === "text/plain") { @@ -965,7 +973,7 @@ const prepareMessages = Effect.fnUntraced( content.push({ type: "tool_result", tool_use_id: part.id, - content: JSON.stringify(part.result), + content: typeof part.result === "string" ? part.result : JSON.stringify(part.result), is_error: part.isFailure, cache_control: cacheControl }) @@ -1290,10 +1298,6 @@ const prepareTools = Effect.fnUntraced( readonly tools: ReadonlyArray | undefined readonly toolChoice: typeof Generated.BetaToolChoice.Encoded | undefined }, AiError.AiError> { - if (options.tools.length === 0 || options.toolChoice === "none") { - return { tools: undefined, toolChoice: undefined } - } - // Return a JSON response tool when using non-native structured outputs if (options.responseFormat.type === "json" && !capabilities.supportsStructuredOutput) { const input_schema = yield* tryJsonSchema(options.responseFormat.schema, "prepareTools") @@ -1313,6 +1317,10 @@ const prepareTools = Effect.fnUntraced( } } + if (options.tools.length === 0 || options.toolChoice === "none") { + return { tools: undefined, toolChoice: undefined } + } + const userTools: Array = [] const providerTools: Array = [] @@ -1567,6 +1575,9 @@ const makeResponse = Effect.fnUntraced( const mcpToolCalls: Map = new Map() const serverToolCalls: Map = new Map() const citableDocuments = extractCitableDocuments(options.prompt) + const responseFormat = options.responseFormat + const hasStructuredOutputTool = responseFormat.type === "json" && + rawResponse.content.some((part) => part.type === "tool_use" && part.name === responseFormat.objectName) parts.push({ type: "response-metadata", @@ -1579,10 +1590,12 @@ const makeResponse = Effect.fnUntraced( for (const part of rawResponse.content) { switch (part.type) { case "text": { - // Text parts are added for both text and json response formats. - // For native structured output (json_schema), the JSON comes directly - // in a text content block. For tool-based structured output, text may - // also be present alongside the tool_use. + // The response tool supplies the JSON payload. Accompanying prose + // must not be concatenated with it during structured output decoding. + if (hasStructuredOutputTool) { + break + } + parts.push({ type: "text", text: part.text @@ -1629,7 +1642,7 @@ const makeResponse = Effect.fnUntraced( case "tool_use": { // When the `"json"` response format is requested, the JSON we need // is returned by a tool call injected into the request - if (options.responseFormat.type === "json") { + if (responseFormat.type === "json" && part.name === responseFormat.objectName) { parts.push({ type: "text", text: JSON.stringify(part.input) @@ -3106,19 +3119,13 @@ const transformToolCallParams = Effect.fnUntraced(function* - ).pipe(Effect.mapError((error) => - AiError.make({ - module: "AnthropicLanguageModel", - method: "makeResponse", - reason: new AiError.ToolParameterValidationError({ - toolName, - toolParams, - description: formatIssue(error.issue) - }) - }) - )) + Schema.decodeEffect(codec)(toolParams) as Effect.Effect + ).pipe( + Effect.flatMap((decoded) => + Schema.encodeUnknownEffect(tool.parametersSchema)(decoded) as Effect.Effect + ), + Effect.orElseSucceed(() => toolParams) + ) }) diff --git a/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts b/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts index bd8b19023..56bab6b2d 100644 --- a/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts +++ b/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts @@ -2,6 +2,7 @@ import { AnthropicClient, AnthropicLanguageModel, AnthropicTool } from "@effect/ import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Redacted, Schema, Stream } from "effect" import { + type AiError, AnthropicStructuredOutput, LanguageModel, Prompt, @@ -119,6 +120,113 @@ describe("AnthropicLanguageModel", () => { assert.deepStrictEqual(toolCall.params, toolParams) })) + it.effect("routes invalid tool call params through failureMode: return without failing the stream", () => + Effect.gen(function*() { + 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: 10, + output_tokens: 0, + service_tier: null + } + } + }, + { + type: "content_block_start", + index: 0, + content_block: { + type: "tool_use", + id: "toolu_test_1", + name: "GlobTool", + input: {} + } + }, + { + type: "content_block_delta", + index: 0, + delta: { + type: "input_json_delta", + partial_json: JSON.stringify({ pattern: 123 }) + } + }, + { + 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: 5 + } + }, + { + type: "message_stop" + } + ])) + ) + )) + ) + + const GlobTool = Tool.make("GlobTool", { + description: "Search for files", + failureMode: "return", + parameters: Schema.Struct({ pattern: Schema.String }), + success: Schema.String, + failure: Schema.String + }) + + const toolkit = Toolkit.make(GlobTool) + const toolkitLayer = toolkit.toLayer({ + GlobTool: () => Effect.succeed("found.ts") + }) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "find ts files", + toolkit + }).pipe( + Stream.runCollect, + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + const toolResult = parts.find((part) => part.type === "tool-result") + assert.isDefined(toolResult) + if (toolResult?.type !== "tool-result") { + return + } + + assert.strictEqual(toolResult.isFailure, true) + const failure = toolResult.result as AiError.AiError + assert.strictEqual(failure._tag, "AiError") + assert.strictEqual(failure.reason._tag, "ToolParameterValidationError") + })) + const codeExecutionCases = [ { providerName: "bash_code_execution", @@ -329,6 +437,175 @@ describe("AnthropicLanguageModel", () => { }) describe("generateText", () => { + it.effect("omits strictJsonSchema from the request while preserving tool strictness", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const model = "claude-sonnet-4-5" + 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_1", + type: "message", + role: "assistant", + model, + content: [{ type: "text", text: "Hello" }], + 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 + } + })) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: "Hello", + toolkit: Toolkit.make(Tool.make("Search", { + parameters: Schema.Struct({ query: Schema.String }), + success: Schema.String + })), + disableToolCallResolution: true + }).pipe( + Effect.provide(AnthropicLanguageModel.model(model, { strictJsonSchema: false })), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + const body = yield* getRequestBody(capturedRequest) + assert.strictEqual(body.model, model) + assert.strictEqual(body.tools[0].name, "Search") + assert.strictEqual(body.tools[0].strict, false) + assert.notProperty(body, "strictJsonSchema") + })) + + it.effect("preserves string tool results", () => + 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_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [{ type: "text", text: "Done" }], + 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 + } + })) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: [Prompt.toolCallPart({ + id: "call_text", + name: "text_tool", + params: {}, + providerExecuted: false + })] + }, + { + role: "tool", + content: [Prompt.toolResultPart({ + id: "call_text", + name: "text_tool", + result: "PLAIN_TEXT_SENTINEL\n", + isFailure: false, + providerExecuted: false + })] + } + ]), + 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 toolResult = body.messages + .flatMap((message: any) => Array.isArray(message.content) ? message.content : []) + .find((block: any) => block.type === "tool_result") + + assert.isDefined(toolResult) + assert.strictEqual(toolResult.content, "PLAIN_TEXT_SENTINEL\n") + })) + + it.effect("preserves base64 image string payloads", () => + Effect.gen(function*() { + const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGP4DwQACfsD/fteaysAAAAASUVORK5CYII=" + 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_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [{ type: "text", text: "Done" }], + 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 + } + })) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([Prompt.userMessage({ + content: [Prompt.filePart({ mediaType: "image/png", data: base64 })] + })]) + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + const body = yield* getRequestBody(capturedRequest) + assert.strictEqual(body.messages[0].content[0].source.data, base64) + })) + it.effect("encodes dynamic tools", () => Effect.gen(function*() { let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined @@ -644,6 +921,94 @@ describe("AnthropicLanguageModel", () => { assert.strictEqual(body.output_config?.format?.type, "json_schema") assert.notProperty(body, "structuredOutputs") })) + + const summaryResponse = (request: HttpClientRequest.HttpClientRequest, content: ReadonlyArray) => + jsonResponse(request, { + id: "msg_summary", + type: "message", + role: "assistant", + model: "claude-sonnet-4-6", + content, + stop_reason: "tool_use", + 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 + } + }) + + it.effect("forces the fallback response tool and decodes its input alongside prose", () => + Effect.gen(function*() { + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.gen(function*() { + const body = yield* getRequestBody(request) + assert.deepStrictEqual(body.tools?.map((tool: { name: string }) => tool.name), ["summary"]) + assert.deepStrictEqual(body.tool_choice, { + type: "tool", + name: "summary", + disable_parallel_tool_use: true + }) + return summaryResponse(request, [ + { type: "text", text: "Here is the summary." }, + { type: "tool_use", id: "toolu_summary", name: "summary", input: { title: "Rain" } } + ]) + }) + ) + )) + ) + + const response = yield* LanguageModel.generateObject({ + prompt: "Give a title for a story about rain.", + objectName: "summary", + schema: Schema.Struct({ title: Schema.String }) + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-6", { structuredOutputs: false })), + Effect.provide(layer) + ) + + assert.deepStrictEqual(response.value, { title: "Rain" }) + })) + + it.effect("decodes native JSON alongside an ordinary tool call", () => + Effect.gen(function*() { + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(summaryResponse(request, [ + { type: "text", text: JSON.stringify({ title: "Rain" }) }, + { type: "tool_use", id: "toolu_weather", name: "Weather", input: { city: "SF" } } + ])) + ) + )) + ) + const toolkit = Toolkit.make(Tool.make("Weather", { + parameters: Schema.Struct({ city: Schema.String }), + success: Schema.String + })) + + const response = yield* LanguageModel.generateObject({ + prompt: "Give a title for a story about rain.", + objectName: "summary", + schema: Schema.Struct({ title: Schema.String }), + toolkit + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-6")), + Effect.provide(toolkit.toLayer({ Weather: () => Effect.succeed("Rain") })), + Effect.provide(layer) + ) + + assert.deepStrictEqual(response.value, { title: "Rain" }) + assert.strictEqual(response.toolCalls[0]?.name, "Weather") + })) }) // The packaged `Memory_20250818` tool ships `customName: "AnthropicMemory"` / diff --git a/repos/effect/packages/ai/openai-compat/CHANGELOG.md b/repos/effect/packages/ai/openai-compat/CHANGELOG.md index 363f05ad6..bdb297955 100644 --- a/repos/effect/packages/ai/openai-compat/CHANGELOG.md +++ b/repos/effect/packages/ai/openai-compat/CHANGELOG.md @@ -1,5 +1,43 @@ # @effect/ai-openai-compat +## 4.0.0-rc.117 + +### Patch Changes + +- Updated dependencies [[`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9), [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724), [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0), [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b), [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9), [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a), [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5), [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741)]: + - effect@4.0.0-rc.117 + +## 4.0.0-rc.116 + +### Patch Changes + +- Updated dependencies [[`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f), [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2), [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d), [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356), [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf), [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b), [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f), [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a), [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac), [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06), [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8), [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153), [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537), [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746), [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9), [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c), [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3), [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b), [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f), [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5), [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55), [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b)]: + - effect@4.0.0-rc.116 + +## 4.0.0-rc.115 + +### Patch Changes + +- Updated dependencies [[`657254b`](https://github.com/Effect-TS/effect/commit/657254b8218628b0116497d09aaf783cb88279f3), [`f9ef0e9`](https://github.com/Effect-TS/effect/commit/f9ef0e970260b5de143392d00593e6c8b4614cff), [`4f73f9e`](https://github.com/Effect-TS/effect/commit/4f73f9e0bd9538260f0f7166e0e6abdbd507a0e8)]: + - effect@4.0.0-rc.115 + +## 4.0.0-rc.114 + +### Patch Changes + +- Updated dependencies [[`3ff4952`](https://github.com/Effect-TS/effect/commit/3ff49521afa6fe6c85c2c230f7e986f7374ef353), [`6d55555`](https://github.com/Effect-TS/effect/commit/6d55555b83701c42855758944985a6fdbf57fffe), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`d4e4ad5`](https://github.com/Effect-TS/effect/commit/d4e4ad57d06d0a30d5e8090ed951f080bcd3a3d7), [`b1988f4`](https://github.com/Effect-TS/effect/commit/b1988f496eef07482f10f4cc03ef20b9570ea0ac), [`482b7d7`](https://github.com/Effect-TS/effect/commit/482b7d7eb08ebe6bc57781414a5a9a12600b2c71), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`9941e6d`](https://github.com/Effect-TS/effect/commit/9941e6dbf800cfd3723bfb421306e04e36b5882d)]: + - effect@4.0.0-rc.114 + +## 4.0.0-rc.113 + +### Patch Changes + +- [#7588](https://github.com/Effect-TS/effect/pull/7588) [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494) Thanks @tim-smart! - Route tool call parameter validation failures through the tool's `failureMode` and drop `ToolParameterValidationError.toolParams`. + +- [#7756](https://github.com/Effect-TS/effect/pull/7756) [`6852750`](https://github.com/Effect-TS/effect/commit/685275058aea20b139498bba7eedd151f5638d7b) Thanks @kitlangton! - Preserve image data supplied as strings in OpenAI Responses and OpenAI-compatible chat requests. +- Updated dependencies [[`49e3901`](https://github.com/Effect-TS/effect/commit/49e39010ff84acbde32c50d559a2f76426cf830e), [`b945ded`](https://github.com/Effect-TS/effect/commit/b945ded23aa9a0ad88bb55aa4089680866dccf92), [`d6422f4`](https://github.com/Effect-TS/effect/commit/d6422f4104062e3e176abe92ef0e444cd4cc6bab), [`5a80204`](https://github.com/Effect-TS/effect/commit/5a802043984727b0c5a291af39d1b9bbfa8d7b8b), [`53511ef`](https://github.com/Effect-TS/effect/commit/53511efcd1c7a05d34b4c9a19c19ed317ebc5d4c), [`79ae49f`](https://github.com/Effect-TS/effect/commit/79ae49f19cab9a42c8f27ae8b104d5e39e6131fa), [`0d083ba`](https://github.com/Effect-TS/effect/commit/0d083ba26b2e1afec8d3e8d83db0d05683b6602b), [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803), [`debe8fd`](https://github.com/Effect-TS/effect/commit/debe8fdc15d00d51cf707353365d7b877123eec6), [`a8588f9`](https://github.com/Effect-TS/effect/commit/a8588f99a43dc9b85dacb14f7be5d3e1ed994d4f), [`f17eb0a`](https://github.com/Effect-TS/effect/commit/f17eb0a6f669e09647c5ded5832ea6baac596659), [`f30cbfe`](https://github.com/Effect-TS/effect/commit/f30cbfe53951128ad8124ae733f12423dbc78bd8), [`78cc9c0`](https://github.com/Effect-TS/effect/commit/78cc9c0d0f36b59f7034b2ace56a4a5c02b7a551), [`ccbdbd5`](https://github.com/Effect-TS/effect/commit/ccbdbd51e7bdb114302701fbc03021868b71956a), [`a63dcbf`](https://github.com/Effect-TS/effect/commit/a63dcbf04e5c3d8d934a41bc6122e9951b1cefa9), [`b845b18`](https://github.com/Effect-TS/effect/commit/b845b186379106c17ed4b1149e2462cd0d2d77b5), [`381b794`](https://github.com/Effect-TS/effect/commit/381b79464f8e0a520585fdcfdd3e95e0553d0b27), [`4ffcaf4`](https://github.com/Effect-TS/effect/commit/4ffcaf4c4a94d3d9d5715db91aa37d7f5d9ecd99), [`ba2fd82`](https://github.com/Effect-TS/effect/commit/ba2fd822e9618f30d0f430ae4eed2eaa4813e2e3), [`02be94c`](https://github.com/Effect-TS/effect/commit/02be94ce78a3c9fc1e4c516d37d8e11e1c0eef2e), [`115d8c2`](https://github.com/Effect-TS/effect/commit/115d8c22599640ece2fd6a10564925b1d79f8a8c), [`1452635`](https://github.com/Effect-TS/effect/commit/14526354f3d0288bd1d0a85c8d4aeee8abe2cd9b), [`77f85fe`](https://github.com/Effect-TS/effect/commit/77f85fe1613348f5c990016b49dc97e252576c82), [`a3f2b31`](https://github.com/Effect-TS/effect/commit/a3f2b319343f28edb028270f7074066cea0ec429), [`310f8d3`](https://github.com/Effect-TS/effect/commit/310f8d3424a025ce1f1cc5c338232db2362052d8), [`291d616`](https://github.com/Effect-TS/effect/commit/291d616282b666bbba3664143fa1d19e839a6a5d), [`48dbbb2`](https://github.com/Effect-TS/effect/commit/48dbbb22bb2e04f63992fc267dd711a0cbd76d72), [`b43bfd6`](https://github.com/Effect-TS/effect/commit/b43bfd644a3205f1518abbfd006f761e31b3a5f9), [`1c89c78`](https://github.com/Effect-TS/effect/commit/1c89c78f9faa3638f398772f3ed7182aa97a1edd), [`9bbe1a5`](https://github.com/Effect-TS/effect/commit/9bbe1a5940eee4642ffe854356128b617e0a064c), [`dd99ab0`](https://github.com/Effect-TS/effect/commit/dd99ab007e3352761187dae330d52f65feeff7c0), [`8f397ed`](https://github.com/Effect-TS/effect/commit/8f397ed4e7852026591819cc4c8c1995bdd2fb8c), [`d7ae6b6`](https://github.com/Effect-TS/effect/commit/d7ae6b6491a88f2710612bfcddaf608ebe925f7c), [`8cf1203`](https://github.com/Effect-TS/effect/commit/8cf12030e8a3e1e76384884d41afa96e2bd72b13), [`87654c5`](https://github.com/Effect-TS/effect/commit/87654c5eed150387f4eac7153751b6085616ba52), [`f05ae0b`](https://github.com/Effect-TS/effect/commit/f05ae0b3fd2cfc7f12b668af244b3cf3fae59bc5), [`436f5eb`](https://github.com/Effect-TS/effect/commit/436f5eb44aa234bdf1de37b07da2ce8dc31ad4ec), [`d8ff960`](https://github.com/Effect-TS/effect/commit/d8ff9605bb5ef7b0cacd80e3b43ed1ce9e3d5e36), [`8766475`](https://github.com/Effect-TS/effect/commit/876647502300b9fb79d163de10fd497716d93be0), [`b64f406`](https://github.com/Effect-TS/effect/commit/b64f406de13a1b9da4664dbde2dbddaa5f776a05), [`4697aaa`](https://github.com/Effect-TS/effect/commit/4697aaa1d38f9da7fe0bcb62f708183f766fe496), [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494), [`9956f0e`](https://github.com/Effect-TS/effect/commit/9956f0e6f47096a6e31305ec2c7320b8e1a140af), [`5c7eed0`](https://github.com/Effect-TS/effect/commit/5c7eed0c5aa0988716206dd374359223edf828ee), [`183c2ea`](https://github.com/Effect-TS/effect/commit/183c2ea65ee1e9b967c746aa1b0bf2b8d3422de2), [`1e92dbd`](https://github.com/Effect-TS/effect/commit/1e92dbd26f5a74bb32103cbaac0f09460b7954de), [`4eb0fa7`](https://github.com/Effect-TS/effect/commit/4eb0fa72302b0a63ac34c6515a9cdf96338e586a), [`534b8b9`](https://github.com/Effect-TS/effect/commit/534b8b9dba195ec38a4795fe564d5e0876cb6468), [`b76a1cf`](https://github.com/Effect-TS/effect/commit/b76a1cf32b0a742c24c1874137e2da8fed5c48eb), [`acc1e53`](https://github.com/Effect-TS/effect/commit/acc1e5384aa5cb7b072f60308013f078b0099bdf), [`4950a91`](https://github.com/Effect-TS/effect/commit/4950a914a68306531a1111d4cdc4504aa8d3854e), [`c020987`](https://github.com/Effect-TS/effect/commit/c020987a0611c9e610a7eef347648b60026745b8), [`abe95d1`](https://github.com/Effect-TS/effect/commit/abe95d12340e1dd435e5df623b1967bf01938c94), [`027ceb9`](https://github.com/Effect-TS/effect/commit/027ceb91e003fbc36f0559f72e1c614cd9198c1a), [`3d203b7`](https://github.com/Effect-TS/effect/commit/3d203b702e26687d2fea4aaae7cc01dcfb82c05e), [`a29b8f4`](https://github.com/Effect-TS/effect/commit/a29b8f4de5aee58dd9ee645365c04cfe1ea6fa9f), [`ce4aa65`](https://github.com/Effect-TS/effect/commit/ce4aa65409e8500908fb6496eca06270ef5b636f), [`a8ea807`](https://github.com/Effect-TS/effect/commit/a8ea80799a0bf187c577e052fef0b7245494c7bf), [`a3ebb7f`](https://github.com/Effect-TS/effect/commit/a3ebb7fcb208f3933a40a0fd949dbcfdf1ca2921), [`473bd81`](https://github.com/Effect-TS/effect/commit/473bd814a97dba66070435cfe84933bcc143effe), [`53843f6`](https://github.com/Effect-TS/effect/commit/53843f6490f4eebaf1eeb91fdcc5f1c542b0e132), [`c5eca65`](https://github.com/Effect-TS/effect/commit/c5eca6539f0d605acd5ce19437fa88f167d69bb7), [`d6f9eba`](https://github.com/Effect-TS/effect/commit/d6f9eba7e8df0c8bd74762c313402884d94ba146), [`8d1e97a`](https://github.com/Effect-TS/effect/commit/8d1e97adbf5a36b4b53ab56f797c2ec0267a8821), [`b28ab48`](https://github.com/Effect-TS/effect/commit/b28ab48be7cf7f2bd30f9467ccac0ab41260f7ad), [`9960708`](https://github.com/Effect-TS/effect/commit/9960708052fcaaa873c471aea144ca46955e6023), [`8ac53b6`](https://github.com/Effect-TS/effect/commit/8ac53b6ae677f06d88d7d81f932ab5ff9b59a10e), [`84ad49a`](https://github.com/Effect-TS/effect/commit/84ad49a357ef156b4ff46cefc945efbe3e292eff), [`72cfa24`](https://github.com/Effect-TS/effect/commit/72cfa2420977919ba57a8e6de538e01e06d5f380), [`95c2581`](https://github.com/Effect-TS/effect/commit/95c2581e0f78e1537aad21764fe562206ca50adb), [`d5c7cd2`](https://github.com/Effect-TS/effect/commit/d5c7cd2c47b2b01523aaea48d2dd039b89b36114), [`fe4fed1`](https://github.com/Effect-TS/effect/commit/fe4fed1b014c78043a8efe37520c527100910010), [`d150a64`](https://github.com/Effect-TS/effect/commit/d150a643c4f60da2dc19ae813b807d48b580087e), [`05b1e80`](https://github.com/Effect-TS/effect/commit/05b1e80cde3a8da1ff1afe288f8539b72c05bb87), [`414dc90`](https://github.com/Effect-TS/effect/commit/414dc9056a39aa22f7cc27aeb5e631231ce50982), [`3f51acd`](https://github.com/Effect-TS/effect/commit/3f51acdf5e2490d7d0a833cd67ce06d1d867facd), [`d68ff05`](https://github.com/Effect-TS/effect/commit/d68ff05506ab5bb3ec07e1b759ce296423b0dbbe), [`f3cf1e6`](https://github.com/Effect-TS/effect/commit/f3cf1e6c36f1aac8399d9b540aecb6410be3096f), [`6525771`](https://github.com/Effect-TS/effect/commit/6525771eef7df6b71a3ef6e299ee93d798ed3dda), [`4ab4e83`](https://github.com/Effect-TS/effect/commit/4ab4e838a4c9ab3c9cc0d52d3e83de88686601b0), [`f7f1d78`](https://github.com/Effect-TS/effect/commit/f7f1d78e52023bdfbebf76949c550db8bb9411dd), [`47b358a`](https://github.com/Effect-TS/effect/commit/47b358a4b85d22973a7957abd1a859f65b75b7da), [`45ffa72`](https://github.com/Effect-TS/effect/commit/45ffa7221259c7e4be9797b90f6ca569fe7ec8a3), [`6232650`](https://github.com/Effect-TS/effect/commit/62326505a74fcd8fe27c8e2064128d2dea484ecc), [`4b73e1b`](https://github.com/Effect-TS/effect/commit/4b73e1bd5576346d7939be8915f3053e428d1c0e), [`284050c`](https://github.com/Effect-TS/effect/commit/284050c2aa6e78eceb08e5eff1803501cc80a658), [`c85fc0b`](https://github.com/Effect-TS/effect/commit/c85fc0b8f2195780929d6fc8ecdc084faf18736c), [`7999b07`](https://github.com/Effect-TS/effect/commit/7999b078950fda15e235db1e61e8db5eb0632528), [`7d455f5`](https://github.com/Effect-TS/effect/commit/7d455f51acf2f0e9f710f298e14e4d81179a9259), [`d681c2e`](https://github.com/Effect-TS/effect/commit/d681c2eb1bb07d782540286368e8b42533df3063), [`84d2a47`](https://github.com/Effect-TS/effect/commit/84d2a470ecc0fcad7f1d1e7a50464e02af0e6972), [`ed74b18`](https://github.com/Effect-TS/effect/commit/ed74b18b95b0797808f2f7928f413f1cf2279d77), [`2245997`](https://github.com/Effect-TS/effect/commit/2245997ad2214591b7d9674777595092926743cf), [`d386979`](https://github.com/Effect-TS/effect/commit/d386979fedbdd433d28816f9cc74675a9af45189), [`fc9fedf`](https://github.com/Effect-TS/effect/commit/fc9fedf59fc26a8bc6e45c3860a921f3b7e0f971), [`7750dbe`](https://github.com/Effect-TS/effect/commit/7750dbe0d5b9929c6d92b0365b42784b993ea2a6), [`fc91af6`](https://github.com/Effect-TS/effect/commit/fc91af659d0682659e6347eede288f90f618acc0), [`39b9738`](https://github.com/Effect-TS/effect/commit/39b9738727ab4d87298fd640edd138f352c70ee3), [`88093b5`](https://github.com/Effect-TS/effect/commit/88093b57951a1b597599fe4772ab690b36a95818), [`d14c463`](https://github.com/Effect-TS/effect/commit/d14c4631ac82911a8ca0b6c24c14ecb95863ee6b), [`d473bd3`](https://github.com/Effect-TS/effect/commit/d473bd3ee966a3a0c245d0e3c752fd91368b8ff6), [`84864bc`](https://github.com/Effect-TS/effect/commit/84864bc30c9e92a1226f65bb78b0641a7e0acea2), [`e2ae724`](https://github.com/Effect-TS/effect/commit/e2ae72481e755a454f28ce45e90f43b857b7c6e5), [`f921ed3`](https://github.com/Effect-TS/effect/commit/f921ed305c63415dc468e94d9319cc3237119138), [`1df933d`](https://github.com/Effect-TS/effect/commit/1df933d1dee2e09a25f9c27214b31aa94fa9d020), [`829aff9`](https://github.com/Effect-TS/effect/commit/829aff97d71d7900b7765ba2f63e33874b898cdc), [`aa0aba3`](https://github.com/Effect-TS/effect/commit/aa0aba383382ca1b99fbe618dceba63c3a5a6a53), [`a71140f`](https://github.com/Effect-TS/effect/commit/a71140fbe79809aa5311a55b1c6908905dc2df26), [`0276a27`](https://github.com/Effect-TS/effect/commit/0276a27ddbb15d163daf56f059ca9f62c4fd9939), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`c86c999`](https://github.com/Effect-TS/effect/commit/c86c99939e1628813a2f92f0906cfbbe924504e8), [`975f758`](https://github.com/Effect-TS/effect/commit/975f7586f3dcda5743d6570a740013a9fbdce9a7), [`2a3a478`](https://github.com/Effect-TS/effect/commit/2a3a4783bcd2d4e91e3505c7a6c1e398fea89595), [`1e6e206`](https://github.com/Effect-TS/effect/commit/1e6e206f6327a114f55a541775f465c0bc9fbf3e), [`fa6027b`](https://github.com/Effect-TS/effect/commit/fa6027b16e65376078e526fa33116ac0cc82485b), [`7616f73`](https://github.com/Effect-TS/effect/commit/7616f73c16bd45a3a3e2202d3f81c6d1cc63f4e0), [`fc668b6`](https://github.com/Effect-TS/effect/commit/fc668b61e9755545a47582dee6c4425d5652cbb6), [`d425c8c`](https://github.com/Effect-TS/effect/commit/d425c8c1319519e861714bcb9d055b63a9434fad), [`ce120f4`](https://github.com/Effect-TS/effect/commit/ce120f474b87d8a5a787dd7f848177ecd3b960fc), [`248201f`](https://github.com/Effect-TS/effect/commit/248201f7846d855685a4dc1b2f01968981706d69), [`e80d397`](https://github.com/Effect-TS/effect/commit/e80d3972fcca4e986b6497d67406f45ee1be540d), [`f1a941d`](https://github.com/Effect-TS/effect/commit/f1a941d52c2f71973e998883869e4e9ef510abf2), [`73bc3a1`](https://github.com/Effect-TS/effect/commit/73bc3a13b6e1b7a060e6bcd19ae7796443211a80), [`b628bb1`](https://github.com/Effect-TS/effect/commit/b628bb1240a09df2244c3ff6c587f978feb25225), [`6e3ae7b`](https://github.com/Effect-TS/effect/commit/6e3ae7b6359bda08cea74b761fd54224d7af45e2), [`e891247`](https://github.com/Effect-TS/effect/commit/e8912473bd4128714a0f8cb04621f3e12f62a910), [`53e6c73`](https://github.com/Effect-TS/effect/commit/53e6c7311b948a62e93e57f6daee66a8ec24b030), [`1579d6f`](https://github.com/Effect-TS/effect/commit/1579d6fd063e444e4fcdfd08ff194c7485203f9c), [`d3c6b73`](https://github.com/Effect-TS/effect/commit/d3c6b73cc4b78f22fdb2e25c8bebda027551540b), [`4a59c6a`](https://github.com/Effect-TS/effect/commit/4a59c6a559e69d399b00c6acd48c8e1977e34875), [`145d8e1`](https://github.com/Effect-TS/effect/commit/145d8e1013220425b8edf34f7011c73f73e1cdcf), [`f74282c`](https://github.com/Effect-TS/effect/commit/f74282cb08de808b678ce67687acbcd61939d2f6), [`0a38623`](https://github.com/Effect-TS/effect/commit/0a3862349ebfbc41746aa0de5e682992719c1da6), [`0a08ae0`](https://github.com/Effect-TS/effect/commit/0a08ae0626f8124779ec2e32ae2088d28db157c6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`d60c5d4`](https://github.com/Effect-TS/effect/commit/d60c5d40b5954ea1557aef1502b4b87f98bbf134), [`07ffd25`](https://github.com/Effect-TS/effect/commit/07ffd25bd0e82c9e6b72b8e669833fb4531a9da3), [`9b517ad`](https://github.com/Effect-TS/effect/commit/9b517ad28a0c2f213693bcab408658c40e3b6d9b), [`604b1c1`](https://github.com/Effect-TS/effect/commit/604b1c109d6c9e654b9b7fcc98d6b80361400325), [`ccc2e02`](https://github.com/Effect-TS/effect/commit/ccc2e02def5965f5535ca97fac2051f4387c4e13), [`14d810a`](https://github.com/Effect-TS/effect/commit/14d810a726caad8a7b0d42f04a6876f25fbcc005), [`a9d1ee3`](https://github.com/Effect-TS/effect/commit/a9d1ee3d4d51e97ea33440fe6100935d5fd5aada), [`a31adbe`](https://github.com/Effect-TS/effect/commit/a31adbefd6c04c70536de11433edba9edd1f5013), [`7245f87`](https://github.com/Effect-TS/effect/commit/7245f87ace09806d15893df8f0bac8f0b9b817db), [`3a0828b`](https://github.com/Effect-TS/effect/commit/3a0828b058abb4b4646bd880458b4e636d77d1c3), [`cd83544`](https://github.com/Effect-TS/effect/commit/cd83544dd594b99e8ea0d7a7645c086f5861769b), [`6550a07`](https://github.com/Effect-TS/effect/commit/6550a074c5c501d8a95dcd354f17e9785a1ae1eb), [`6f090d4`](https://github.com/Effect-TS/effect/commit/6f090d4c2032cc35f7f9a9eac900d0329b8ac07e), [`b505c0d`](https://github.com/Effect-TS/effect/commit/b505c0db03e86eb126237690a5867f457eb4237f), [`186dd49`](https://github.com/Effect-TS/effect/commit/186dd4914084fa346a870ce9c637b9c2a6cc8100), [`9f37e58`](https://github.com/Effect-TS/effect/commit/9f37e58d00e06150d96af48fffe780947ca0d519), [`4446451`](https://github.com/Effect-TS/effect/commit/44464510114773e1ebd1abb6efffa382cf69998e), [`58be972`](https://github.com/Effect-TS/effect/commit/58be97238ee1f6cd26f6a3a645d1df97a091a224), [`1320075`](https://github.com/Effect-TS/effect/commit/1320075fd4ed381f60f6f37f280dbec17cb29b81), [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80), [`f7490d4`](https://github.com/Effect-TS/effect/commit/f7490d49c75b99118c6c42549511ffa166459f59), [`c8ea602`](https://github.com/Effect-TS/effect/commit/c8ea60231ad69e23c0bf319f6f6b3ff28fc45e4b), [`62d82f4`](https://github.com/Effect-TS/effect/commit/62d82f4919c72d8cc0cfd2b704ed6900f69ad9a8), [`a2c9e7c`](https://github.com/Effect-TS/effect/commit/a2c9e7c17a4118b74a1eab3332042dafe0bb7318), [`97dd022`](https://github.com/Effect-TS/effect/commit/97dd022fe7fbd1696dd60f3319eae594abdd3760), [`f984ee8`](https://github.com/Effect-TS/effect/commit/f984ee82d255c7958eeed6f0dd08de1b88cbbfd6), [`f1b2910`](https://github.com/Effect-TS/effect/commit/f1b291027c3055f801c2f288dda21ba84c361956), [`a29e05a`](https://github.com/Effect-TS/effect/commit/a29e05a907053a8617763b4e7408e1f012b59049), [`1aa1d8b`](https://github.com/Effect-TS/effect/commit/1aa1d8bd0f2c2d429938657927f34f8ff0f940a0), [`bb99734`](https://github.com/Effect-TS/effect/commit/bb99734ef87bcc377c628bc4072aa856ce5f7ca6), [`222e7ca`](https://github.com/Effect-TS/effect/commit/222e7caa4bb0da6d81a4eabf25dffc181c20f6a3), [`797c9e3`](https://github.com/Effect-TS/effect/commit/797c9e3c1f2e66db1570cf6aa5c4a15e94507bf0), [`b4d5398`](https://github.com/Effect-TS/effect/commit/b4d5398598c04a84054a55873c943d587880058d), [`c8349ed`](https://github.com/Effect-TS/effect/commit/c8349ede1a2add807e9ec9c37612b3a39bf284f8), [`8426e5f`](https://github.com/Effect-TS/effect/commit/8426e5f7ad11f15e39565cf8ba0874bf974f5085), [`26e0085`](https://github.com/Effect-TS/effect/commit/26e0085d098f6e526c085c357b8d82ee068b6518), [`1a86166`](https://github.com/Effect-TS/effect/commit/1a86166f102b517ee4d89dca0095e28f738eee10), [`0856631`](https://github.com/Effect-TS/effect/commit/0856631a89d8f72d206c366905ddac75468a8e85), [`0af0985`](https://github.com/Effect-TS/effect/commit/0af0985d5c132b33e14a2df59e0567d7ba2a1710), [`bc582c9`](https://github.com/Effect-TS/effect/commit/bc582c94313cbb89d87b3844ffffabebbb4ffa83), [`2c63f1e`](https://github.com/Effect-TS/effect/commit/2c63f1e07e375b7958e14929c6a4b358c819a9d0), [`0d98213`](https://github.com/Effect-TS/effect/commit/0d9821346453d44696258103643d5482e63638b4), [`ca6f0dc`](https://github.com/Effect-TS/effect/commit/ca6f0dc4fcea564b351bf505df015d16a8b965ed), [`d8cc9ed`](https://github.com/Effect-TS/effect/commit/d8cc9ed80e1df9c056a46ba62df01f7fc0f531c3), [`42fd969`](https://github.com/Effect-TS/effect/commit/42fd969b45025eed6d60cb87b97c5ff88d187671), [`5641ad3`](https://github.com/Effect-TS/effect/commit/5641ad333a88bb9e56baf886006682678391be0a), [`629870d`](https://github.com/Effect-TS/effect/commit/629870d461b36d3dacb30c8d203b8d20d4beaba0), [`d592c14`](https://github.com/Effect-TS/effect/commit/d592c1492ad895ea72537e10387b735f3ba0e7de), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`53909a9`](https://github.com/Effect-TS/effect/commit/53909a9bf14b29725a08dcccde02bee8e95a6b14), [`78a4269`](https://github.com/Effect-TS/effect/commit/78a42697f6085c87c09b18e8095bb7541bd65261), [`06c6307`](https://github.com/Effect-TS/effect/commit/06c6307d3d5538bc398c67a44239bc3d52ab1a4d), [`0847c41`](https://github.com/Effect-TS/effect/commit/0847c41e2472a347cd8f77d460d3cbdfecd4adf0), [`5a77084`](https://github.com/Effect-TS/effect/commit/5a77084651bc58617b9988606473cf9ba5ca0032), [`96f99b3`](https://github.com/Effect-TS/effect/commit/96f99b325da32bfa535e826d89d838d0edec0dea), [`7bb8781`](https://github.com/Effect-TS/effect/commit/7bb878176b8ad20cf0e671b570caf72e194f9515), [`4907e9b`](https://github.com/Effect-TS/effect/commit/4907e9b2470894ddaf0cbdec2612b59f04a51dbe), [`2a30248`](https://github.com/Effect-TS/effect/commit/2a30248b6eb739f22403456209bc468f2f4ef26a), [`8364ddd`](https://github.com/Effect-TS/effect/commit/8364ddda233d35ee472cc2356312317a60a148a2), [`ad67d8c`](https://github.com/Effect-TS/effect/commit/ad67d8c5a1b195ff5468a9f2e963c59aa0b3166f), [`ec0c087`](https://github.com/Effect-TS/effect/commit/ec0c0875262fb81c7992f5023776b8fa894c49b6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`a2c1ce6`](https://github.com/Effect-TS/effect/commit/a2c1ce62f04214d98d848f4cfe33d677e1a87d39), [`91e9af0`](https://github.com/Effect-TS/effect/commit/91e9af0d5f138933d3a131ca1cfbc52d3bc55481), [`7bd3f34`](https://github.com/Effect-TS/effect/commit/7bd3f34385a06157b249a351e32ffdcbe1cf2c37), [`ef16581`](https://github.com/Effect-TS/effect/commit/ef165814eae0a199fdca5b3c2839bb4fcdfd5b5d), [`1693a87`](https://github.com/Effect-TS/effect/commit/1693a877324273f5a23fa7d7fc892f4ee12f20c2), [`df3fc47`](https://github.com/Effect-TS/effect/commit/df3fc47609eda7843dae2b557c8fe834299e26b4), [`e11be41`](https://github.com/Effect-TS/effect/commit/e11be41dfc73fa6b5643ab9af7df18d6907927d7), [`2e39e8b`](https://github.com/Effect-TS/effect/commit/2e39e8bafef7d3fee9f3b324b7d50cc064376667), [`11c5ee7`](https://github.com/Effect-TS/effect/commit/11c5ee7202acd3bb789d0a8ce78e6c4523071553), [`1742d2f`](https://github.com/Effect-TS/effect/commit/1742d2f48441cb7b1a8d9aca3a8e6f0acbc35a96), [`8efc70e`](https://github.com/Effect-TS/effect/commit/8efc70ea16bee43fd9adde2871cf82a8131b80a0), [`9642776`](https://github.com/Effect-TS/effect/commit/964277661423f398f777ec2eab4b3ecc0b53f53b), [`6680828`](https://github.com/Effect-TS/effect/commit/668082885f52672e4c7fad4e0fe2bfa0e69703e2), [`c34edcb`](https://github.com/Effect-TS/effect/commit/c34edcb6d7a6151d53e19828c92abf7dd8450c2a), [`7b2c5bd`](https://github.com/Effect-TS/effect/commit/7b2c5bd3c394fa4751fb229394ac9c03e401f441), [`1a2ccee`](https://github.com/Effect-TS/effect/commit/1a2ccee2bbb93514dc66e0a9cdeb52a82172ab0e), [`db995df`](https://github.com/Effect-TS/effect/commit/db995df19b0381b64cc39b50aa28d7bd8f2c1294), [`af0ccdd`](https://github.com/Effect-TS/effect/commit/af0ccdd13aa8917bce7161b8bc257fbe51c6a6a5), [`addeaea`](https://github.com/Effect-TS/effect/commit/addeaea0b0abe0dc24ca1d4d0fd194a0e458be70), [`7704034`](https://github.com/Effect-TS/effect/commit/770403411bcd8befef0552a0e40066abc417f6ea), [`e72b12f`](https://github.com/Effect-TS/effect/commit/e72b12fc305710550bc6dcb978e92de8abff88cd), [`310dd9c`](https://github.com/Effect-TS/effect/commit/310dd9ce9681e97f558bb042adb35f6040ab81e3), [`1c2afc1`](https://github.com/Effect-TS/effect/commit/1c2afc15bbf4ce5a91d24da99bc275b5bd744e77), [`44f44ca`](https://github.com/Effect-TS/effect/commit/44f44cae65249801c082c3a3848016eeeac5eb24), [`f43b9d6`](https://github.com/Effect-TS/effect/commit/f43b9d6d99cb591c0377bb1474121827201627c9), [`56e72b3`](https://github.com/Effect-TS/effect/commit/56e72b38c01bbe6ec921ad151f83b7849444f219), [`50ef80e`](https://github.com/Effect-TS/effect/commit/50ef80e251b85b5ee63aed06fe0244b5433ec2fa), [`fc3b718`](https://github.com/Effect-TS/effect/commit/fc3b7187114a31cbb21a250f55fda601bb3edcf0), [`ee336d8`](https://github.com/Effect-TS/effect/commit/ee336d80b4024b51654e7a2b8d869d244153fcdf), [`d12f922`](https://github.com/Effect-TS/effect/commit/d12f922853eff2310dbe9fc9b31562ad81b38435), [`46d8310`](https://github.com/Effect-TS/effect/commit/46d83101e8408c0428d9a8760e06e23a69ad6070), [`59812fd`](https://github.com/Effect-TS/effect/commit/59812fd221cde16fc77c03d326c6a2cf8fbec6b9), [`f9d0dec`](https://github.com/Effect-TS/effect/commit/f9d0dec12c5f229f05d7a74d532a85c6bbffefbf), [`4372c79`](https://github.com/Effect-TS/effect/commit/4372c79a32fcecc7b6b284d49a215b2c4fef9d77), [`81485ef`](https://github.com/Effect-TS/effect/commit/81485ef0288a3321e75d7d4ae32b7e0e06b6f86b), [`bd393d6`](https://github.com/Effect-TS/effect/commit/bd393d63c19bdd0ab212d95576cec89051c8501c), [`0c95c04`](https://github.com/Effect-TS/effect/commit/0c95c04b41c2d5d09e4564fd997572207fd72595)]: + - effect@4.0.0-rc.113 + ## 4.0.0-rc.112 ### Patch Changes diff --git a/repos/effect/packages/ai/openai-compat/package.json b/repos/effect/packages/ai/openai-compat/package.json index e68e72f5c..c83e71100 100644 --- a/repos/effect/packages/ai/openai-compat/package.json +++ b/repos/effect/packages/ai/openai-compat/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai-compat", - "version": "4.0.0-rc.112", + "version": "4.0.0-rc.117", "type": "module", "license": "MIT", "description": "An OpenAI compat integration for Effect", diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts index 0cd5393aa..c64569c4a 100644 --- a/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts @@ -118,7 +118,7 @@ export const model = ( export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { readonly model: string readonly config?: ModelConfig | undefined -}): Effect.fn.Return { +}): Effect.fn.Return { const client = yield* OpenAiClient const makeConfig = Effect.contextWith((services: Context.Context) => diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts index 71e1e586a..923f39397 100644 --- a/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts @@ -20,7 +20,6 @@ import * as Rec from "effect/Record" 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, Simplify } from "effect/Types" @@ -55,8 +54,6 @@ import { } from "./OpenAiClient.ts" import { addGenAIAnnotations } from "./OpenAiTelemetry.ts" -const formatIssue = SchemaIssue.makeFormatterDefault() - /** * Image detail level for vision requests. */ @@ -575,7 +572,7 @@ export const model = ( export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { readonly model: string readonly config?: ModelConfig | undefined -}): Effect.fn.Return { +}): Effect.fn.Return { const client = yield* OpenAiClient const makeConfig = Effect.contextWith((services: Context.Context) => @@ -790,15 +787,14 @@ const prepareMessages = Effect.fnUntraced( if (typeof part.data === "string" && isFileId(part.data, config)) { content.push({ type: "input_image", file_id: part.data, detail }) - } - - if (part.data instanceof URL) { - content.push({ type: "input_image", image_url: part.data.toString(), detail }) - } - - if (part.data instanceof Uint8Array) { - const base64 = Encoding.encodeBase64(part.data) - const imageUrl = `data:${mediaType};base64,${base64}` + } else { + const imageUrl = part.data instanceof URL + ? part.data.toString() + : part.data instanceof Uint8Array + ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}` + : /^(data:|https?:\/\/)/i.test(part.data) + ? part.data + : `data:${mediaType};base64,${part.data}` content.push({ type: "input_image", image_url: imageUrl, detail }) } } else if (part.mediaType === "application/pdf") { @@ -1099,7 +1095,6 @@ const makeResponse = Effect.fnUntraced( method: "makeResponse", reason: new AiError.ToolParameterValidationError({ toolName, - toolParams: {}, description: `Failed to securely JSON parse tool parameters: ${cause}` }) }) @@ -1192,7 +1187,6 @@ const makeStreamResponse = Effect.fnUntraced( method: "makeStreamResponse", reason: new AiError.ToolParameterValidationError({ toolName: toolCall.name, - toolParams: {}, description: `Failed to securely JSON parse tool parameters: ${cause}` }) }) @@ -1452,21 +1446,16 @@ const transformToolCallParams = Effect.fnUntraced(function* - ).pipe(Effect.mapError((error) => - AiError.make({ - module: "OpenAiLanguageModel", - method: "makeResponse", - reason: new AiError.ToolParameterValidationError({ - toolName, - toolParams, - description: formatIssue(error.issue) - }) - }) - )) + Schema.decodeEffect(codec)(toolParams) as Effect.Effect + ).pipe( + Effect.flatMap((decoded) => + Schema.encodeUnknownEffect(tool.parametersSchema)(decoded) as Effect.Effect + ), + Effect.orElseSucceed(() => toolParams) + ) }) const prepareTools = Effect.fnUntraced(function*>({ diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts b/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts index df6edf923..dc90d1f4d 100644 --- a/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts +++ b/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts @@ -1,7 +1,7 @@ import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai-compat" import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Redacted, Ref, Schema, Stream } from "effect" -import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" +import { type AiError, LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("OpenAiLanguageModel", () => { @@ -48,6 +48,67 @@ describe("OpenAiLanguageModel", () => { assert.strictEqual(requestBody.messages[0]?.content, "hello") })) + it.effect("routes invalid tool call params through failureMode: return without failing the effect", () => + 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_1", + type: "function", + function: { + name: "ReturnModeTool", + arguments: JSON.stringify({ input: 123 }) + } + }] + } + }] + }) + )) + ) + )) + ) + + const ReturnModeTool = Tool.make("ReturnModeTool", { + description: "A test tool", + failureMode: "return", + parameters: Schema.Struct({ input: Schema.String }), + success: Schema.Struct({ output: Schema.String }), + failure: Schema.Struct({ error: Schema.String }) + }) + + const toolkit = Toolkit.make(ReturnModeTool) + const toolkitLayer = toolkit.toLayer({ + ReturnModeTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "use the tool", + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + assert.strictEqual(result.toolResults.length, 1) + const toolResult = result.toolResults[0]! + assert.strictEqual(toolResult.isFailure, true) + const failure = toolResult.result as AiError.AiError + assert.strictEqual(failure._tag, "AiError") + assert.strictEqual(failure.reason._tag, "ToolParameterValidationError") + })) + it.effect("forwards reasoning config to chat completions request", () => Effect.gen(function*() { let capturedRequest: HttpClientRequest.HttpClientRequest | undefined @@ -142,8 +203,9 @@ describe("OpenAiLanguageModel", () => { }) })) - it.effect("preserves multimodal user content order in chat payload", () => + it.effect("preserves URL and base64 images in multimodal content order", () => Effect.gen(function*() { + const base64 = "iVBORw0KGgo=" let capturedRequest: HttpClientRequest.HttpClientRequest | undefined const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( @@ -177,6 +239,8 @@ describe("OpenAiLanguageModel", () => { mediaType: "image/png", data: new URL("https://example.com/image.png") }), + Prompt.filePart({ mediaType: "image/png", data: "https://example.com/string-image.png" }), + Prompt.filePart({ mediaType: "image/png", data: base64 }), Prompt.textPart({ text: "second text" }) ] }]) @@ -205,6 +269,20 @@ describe("OpenAiLanguageModel", () => { detail: "auto" } }, + { + type: "image_url", + image_url: { + url: "https://example.com/string-image.png", + detail: "auto" + } + }, + { + type: "image_url", + image_url: { + url: `data:image/png;base64,${base64}`, + detail: "auto" + } + }, { type: "text", text: "second text" diff --git a/repos/effect/packages/ai/openai/CHANGELOG.md b/repos/effect/packages/ai/openai/CHANGELOG.md index cb5fb0a47..7cf475a6e 100644 --- a/repos/effect/packages/ai/openai/CHANGELOG.md +++ b/repos/effect/packages/ai/openai/CHANGELOG.md @@ -1,5 +1,65 @@ # @effect/ai-openai +## 4.0.0-rc.117 + +### Patch Changes + +- Updated dependencies [[`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9), [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724), [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0), [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b), [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9), [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a), [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5), [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741)]: + - effect@4.0.0-rc.117 + +## 4.0.0-rc.116 + +### Patch Changes + +- [#8236](https://github.com/Effect-TS/effect/pull/8236) [`90695b0`](https://github.com/Effect-TS/effect/commit/90695b0d5bee3aa33e67cbea1aa2e6298816a64f) Thanks @danieljvdm! - Support API sources in OpenAI web search. Narrow sources by `type` before reading `url` or `name`. +- Updated dependencies [[`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f), [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2), [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d), [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356), [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf), [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b), [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f), [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a), [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac), [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06), [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8), [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153), [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537), [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746), [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9), [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c), [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3), [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b), [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f), [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5), [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55), [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b)]: + - effect@4.0.0-rc.116 + +## 4.0.0-rc.115 + +### Patch Changes + +- Updated dependencies [[`657254b`](https://github.com/Effect-TS/effect/commit/657254b8218628b0116497d09aaf783cb88279f3), [`f9ef0e9`](https://github.com/Effect-TS/effect/commit/f9ef0e970260b5de143392d00593e6c8b4614cff), [`4f73f9e`](https://github.com/Effect-TS/effect/commit/4f73f9e0bd9538260f0f7166e0e6abdbd507a0e8)]: + - effect@4.0.0-rc.115 + +## 4.0.0-rc.114 + +### Patch Changes + +- [#8157](https://github.com/Effect-TS/effect/pull/8157) [`fdec3b2`](https://github.com/Effect-TS/effect/commit/fdec3b23a163566b65ce67b721b4d2faa009001c) Thanks @danieljvdm! - Report failed OpenAI provider-executed tool calls as failure tool results in both `generateText` and `streamText`, preserving the call details. +- Updated dependencies [[`3ff4952`](https://github.com/Effect-TS/effect/commit/3ff49521afa6fe6c85c2c230f7e986f7374ef353), [`6d55555`](https://github.com/Effect-TS/effect/commit/6d55555b83701c42855758944985a6fdbf57fffe), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`d4e4ad5`](https://github.com/Effect-TS/effect/commit/d4e4ad57d06d0a30d5e8090ed951f080bcd3a3d7), [`b1988f4`](https://github.com/Effect-TS/effect/commit/b1988f496eef07482f10f4cc03ef20b9570ea0ac), [`482b7d7`](https://github.com/Effect-TS/effect/commit/482b7d7eb08ebe6bc57781414a5a9a12600b2c71), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`9941e6d`](https://github.com/Effect-TS/effect/commit/9941e6dbf800cfd3723bfb421306e04e36b5882d)]: + - effect@4.0.0-rc.114 + +## 4.0.0-rc.113 + +### Patch Changes + +- [#7477](https://github.com/Effect-TS/effect/pull/7477) [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803) Thanks @candrewlee14! - Allow sockets to use browser, Bun, and Node WebSocket implementations without consumer casts. Platform constructors + now support typed opening-handshake headers where available. + +- [#7588](https://github.com/Effect-TS/effect/pull/7588) [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494) Thanks @tim-smart! - Route tool call parameter validation failures through the tool's `failureMode` and drop `ToolParameterValidationError.toolParams`. + +- [#7756](https://github.com/Effect-TS/effect/pull/7756) [`6852750`](https://github.com/Effect-TS/effect/commit/685275058aea20b139498bba7eedd151f5638d7b) Thanks @kitlangton! - Preserve image data supplied as strings in OpenAI Responses and OpenAI-compatible chat requests. + +- [#8037](https://github.com/Effect-TS/effect/pull/8037) [`e255f08`](https://github.com/Effect-TS/effect/commit/e255f0893e5f185414466b6a06b710c554ef8c7c) Thanks @tim-smart! - Allow OpenAI-compatible Responses API stream events to omit `sequence_number`. + +- [#7805](https://github.com/Effect-TS/effect/pull/7805) [`0b93584`](https://github.com/Effect-TS/effect/commit/0b935848082dd178db8278bff52a2cd07e5bf69a) Thanks @tim-smart! - Preserve plain-text client tool results when encoding provider requests. + +- [#7487](https://github.com/Effect-TS/effect/pull/7487) [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80) Thanks @tim-smart! - Redesign `Socket` around a scoped, pull-based reader with transport backpressure. + + `Socket` now exposes `reader` and `writer`. Client reader acquisition dials and yields a pull of non-empty batches: one buffer for TCP and one entry per WebSocket frame. TCP applies backpressure while paused; pausable WebSockets pause at `highWaterMark` (64 KiB by default) and resume after draining. Browser WebSockets cannot pause, so they can fail with `SocketReadError` at a configured `highWaterMark`. Writes await native drain signals and batch with `cork` / `uncork` where available. + + ### Breaking changes + + - `Socket.run`, `Socket.runString`, and `Socket.runRaw` are removed. Acquire `socket.reader` (or `Socket.readerBytes` / `Socket.readerString`) in a scope and pull in a loop. Code before the first pull replaces `onOpen`. + - `Socket.make` now takes `{ reader, writer }`. The writer acquisition is infallible and yields a `Writer` with `write` and `writeAll`; both operations can still fail with `SocketError`. + - Every close fails the pull with `SocketError` wrapping `SocketCloseError`. The close-code predicates are removed; use `Effect.retry` around the scoped read loop to reconnect. + - `Socket.toChannel` and `Socket.toChannelString` now read from the pull and fail on close. `Socket.toStream` is added for read-only consumption. + - `fromWebSocket` drops the `onInitialRun` option; `SendQueueCapacity` is removed. + - Accepted server sockets pause immediately. Their reader attaches to the existing connection and cannot reconnect after close. +- Updated dependencies [[`49e3901`](https://github.com/Effect-TS/effect/commit/49e39010ff84acbde32c50d559a2f76426cf830e), [`b945ded`](https://github.com/Effect-TS/effect/commit/b945ded23aa9a0ad88bb55aa4089680866dccf92), [`d6422f4`](https://github.com/Effect-TS/effect/commit/d6422f4104062e3e176abe92ef0e444cd4cc6bab), [`5a80204`](https://github.com/Effect-TS/effect/commit/5a802043984727b0c5a291af39d1b9bbfa8d7b8b), [`53511ef`](https://github.com/Effect-TS/effect/commit/53511efcd1c7a05d34b4c9a19c19ed317ebc5d4c), [`79ae49f`](https://github.com/Effect-TS/effect/commit/79ae49f19cab9a42c8f27ae8b104d5e39e6131fa), [`0d083ba`](https://github.com/Effect-TS/effect/commit/0d083ba26b2e1afec8d3e8d83db0d05683b6602b), [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803), [`debe8fd`](https://github.com/Effect-TS/effect/commit/debe8fdc15d00d51cf707353365d7b877123eec6), [`a8588f9`](https://github.com/Effect-TS/effect/commit/a8588f99a43dc9b85dacb14f7be5d3e1ed994d4f), [`f17eb0a`](https://github.com/Effect-TS/effect/commit/f17eb0a6f669e09647c5ded5832ea6baac596659), [`f30cbfe`](https://github.com/Effect-TS/effect/commit/f30cbfe53951128ad8124ae733f12423dbc78bd8), [`78cc9c0`](https://github.com/Effect-TS/effect/commit/78cc9c0d0f36b59f7034b2ace56a4a5c02b7a551), [`ccbdbd5`](https://github.com/Effect-TS/effect/commit/ccbdbd51e7bdb114302701fbc03021868b71956a), [`a63dcbf`](https://github.com/Effect-TS/effect/commit/a63dcbf04e5c3d8d934a41bc6122e9951b1cefa9), [`b845b18`](https://github.com/Effect-TS/effect/commit/b845b186379106c17ed4b1149e2462cd0d2d77b5), [`381b794`](https://github.com/Effect-TS/effect/commit/381b79464f8e0a520585fdcfdd3e95e0553d0b27), [`4ffcaf4`](https://github.com/Effect-TS/effect/commit/4ffcaf4c4a94d3d9d5715db91aa37d7f5d9ecd99), [`ba2fd82`](https://github.com/Effect-TS/effect/commit/ba2fd822e9618f30d0f430ae4eed2eaa4813e2e3), [`02be94c`](https://github.com/Effect-TS/effect/commit/02be94ce78a3c9fc1e4c516d37d8e11e1c0eef2e), [`115d8c2`](https://github.com/Effect-TS/effect/commit/115d8c22599640ece2fd6a10564925b1d79f8a8c), [`1452635`](https://github.com/Effect-TS/effect/commit/14526354f3d0288bd1d0a85c8d4aeee8abe2cd9b), [`77f85fe`](https://github.com/Effect-TS/effect/commit/77f85fe1613348f5c990016b49dc97e252576c82), [`a3f2b31`](https://github.com/Effect-TS/effect/commit/a3f2b319343f28edb028270f7074066cea0ec429), [`310f8d3`](https://github.com/Effect-TS/effect/commit/310f8d3424a025ce1f1cc5c338232db2362052d8), [`291d616`](https://github.com/Effect-TS/effect/commit/291d616282b666bbba3664143fa1d19e839a6a5d), [`48dbbb2`](https://github.com/Effect-TS/effect/commit/48dbbb22bb2e04f63992fc267dd711a0cbd76d72), [`b43bfd6`](https://github.com/Effect-TS/effect/commit/b43bfd644a3205f1518abbfd006f761e31b3a5f9), [`1c89c78`](https://github.com/Effect-TS/effect/commit/1c89c78f9faa3638f398772f3ed7182aa97a1edd), [`9bbe1a5`](https://github.com/Effect-TS/effect/commit/9bbe1a5940eee4642ffe854356128b617e0a064c), [`dd99ab0`](https://github.com/Effect-TS/effect/commit/dd99ab007e3352761187dae330d52f65feeff7c0), [`8f397ed`](https://github.com/Effect-TS/effect/commit/8f397ed4e7852026591819cc4c8c1995bdd2fb8c), [`d7ae6b6`](https://github.com/Effect-TS/effect/commit/d7ae6b6491a88f2710612bfcddaf608ebe925f7c), [`8cf1203`](https://github.com/Effect-TS/effect/commit/8cf12030e8a3e1e76384884d41afa96e2bd72b13), [`87654c5`](https://github.com/Effect-TS/effect/commit/87654c5eed150387f4eac7153751b6085616ba52), [`f05ae0b`](https://github.com/Effect-TS/effect/commit/f05ae0b3fd2cfc7f12b668af244b3cf3fae59bc5), [`436f5eb`](https://github.com/Effect-TS/effect/commit/436f5eb44aa234bdf1de37b07da2ce8dc31ad4ec), [`d8ff960`](https://github.com/Effect-TS/effect/commit/d8ff9605bb5ef7b0cacd80e3b43ed1ce9e3d5e36), [`8766475`](https://github.com/Effect-TS/effect/commit/876647502300b9fb79d163de10fd497716d93be0), [`b64f406`](https://github.com/Effect-TS/effect/commit/b64f406de13a1b9da4664dbde2dbddaa5f776a05), [`4697aaa`](https://github.com/Effect-TS/effect/commit/4697aaa1d38f9da7fe0bcb62f708183f766fe496), [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494), [`9956f0e`](https://github.com/Effect-TS/effect/commit/9956f0e6f47096a6e31305ec2c7320b8e1a140af), [`5c7eed0`](https://github.com/Effect-TS/effect/commit/5c7eed0c5aa0988716206dd374359223edf828ee), [`183c2ea`](https://github.com/Effect-TS/effect/commit/183c2ea65ee1e9b967c746aa1b0bf2b8d3422de2), [`1e92dbd`](https://github.com/Effect-TS/effect/commit/1e92dbd26f5a74bb32103cbaac0f09460b7954de), [`4eb0fa7`](https://github.com/Effect-TS/effect/commit/4eb0fa72302b0a63ac34c6515a9cdf96338e586a), [`534b8b9`](https://github.com/Effect-TS/effect/commit/534b8b9dba195ec38a4795fe564d5e0876cb6468), [`b76a1cf`](https://github.com/Effect-TS/effect/commit/b76a1cf32b0a742c24c1874137e2da8fed5c48eb), [`acc1e53`](https://github.com/Effect-TS/effect/commit/acc1e5384aa5cb7b072f60308013f078b0099bdf), [`4950a91`](https://github.com/Effect-TS/effect/commit/4950a914a68306531a1111d4cdc4504aa8d3854e), [`c020987`](https://github.com/Effect-TS/effect/commit/c020987a0611c9e610a7eef347648b60026745b8), [`abe95d1`](https://github.com/Effect-TS/effect/commit/abe95d12340e1dd435e5df623b1967bf01938c94), [`027ceb9`](https://github.com/Effect-TS/effect/commit/027ceb91e003fbc36f0559f72e1c614cd9198c1a), [`3d203b7`](https://github.com/Effect-TS/effect/commit/3d203b702e26687d2fea4aaae7cc01dcfb82c05e), [`a29b8f4`](https://github.com/Effect-TS/effect/commit/a29b8f4de5aee58dd9ee645365c04cfe1ea6fa9f), [`ce4aa65`](https://github.com/Effect-TS/effect/commit/ce4aa65409e8500908fb6496eca06270ef5b636f), [`a8ea807`](https://github.com/Effect-TS/effect/commit/a8ea80799a0bf187c577e052fef0b7245494c7bf), [`a3ebb7f`](https://github.com/Effect-TS/effect/commit/a3ebb7fcb208f3933a40a0fd949dbcfdf1ca2921), [`473bd81`](https://github.com/Effect-TS/effect/commit/473bd814a97dba66070435cfe84933bcc143effe), [`53843f6`](https://github.com/Effect-TS/effect/commit/53843f6490f4eebaf1eeb91fdcc5f1c542b0e132), [`c5eca65`](https://github.com/Effect-TS/effect/commit/c5eca6539f0d605acd5ce19437fa88f167d69bb7), [`d6f9eba`](https://github.com/Effect-TS/effect/commit/d6f9eba7e8df0c8bd74762c313402884d94ba146), [`8d1e97a`](https://github.com/Effect-TS/effect/commit/8d1e97adbf5a36b4b53ab56f797c2ec0267a8821), [`b28ab48`](https://github.com/Effect-TS/effect/commit/b28ab48be7cf7f2bd30f9467ccac0ab41260f7ad), [`9960708`](https://github.com/Effect-TS/effect/commit/9960708052fcaaa873c471aea144ca46955e6023), [`8ac53b6`](https://github.com/Effect-TS/effect/commit/8ac53b6ae677f06d88d7d81f932ab5ff9b59a10e), [`84ad49a`](https://github.com/Effect-TS/effect/commit/84ad49a357ef156b4ff46cefc945efbe3e292eff), [`72cfa24`](https://github.com/Effect-TS/effect/commit/72cfa2420977919ba57a8e6de538e01e06d5f380), [`95c2581`](https://github.com/Effect-TS/effect/commit/95c2581e0f78e1537aad21764fe562206ca50adb), [`d5c7cd2`](https://github.com/Effect-TS/effect/commit/d5c7cd2c47b2b01523aaea48d2dd039b89b36114), [`fe4fed1`](https://github.com/Effect-TS/effect/commit/fe4fed1b014c78043a8efe37520c527100910010), [`d150a64`](https://github.com/Effect-TS/effect/commit/d150a643c4f60da2dc19ae813b807d48b580087e), [`05b1e80`](https://github.com/Effect-TS/effect/commit/05b1e80cde3a8da1ff1afe288f8539b72c05bb87), [`414dc90`](https://github.com/Effect-TS/effect/commit/414dc9056a39aa22f7cc27aeb5e631231ce50982), [`3f51acd`](https://github.com/Effect-TS/effect/commit/3f51acdf5e2490d7d0a833cd67ce06d1d867facd), [`d68ff05`](https://github.com/Effect-TS/effect/commit/d68ff05506ab5bb3ec07e1b759ce296423b0dbbe), [`f3cf1e6`](https://github.com/Effect-TS/effect/commit/f3cf1e6c36f1aac8399d9b540aecb6410be3096f), [`6525771`](https://github.com/Effect-TS/effect/commit/6525771eef7df6b71a3ef6e299ee93d798ed3dda), [`4ab4e83`](https://github.com/Effect-TS/effect/commit/4ab4e838a4c9ab3c9cc0d52d3e83de88686601b0), [`f7f1d78`](https://github.com/Effect-TS/effect/commit/f7f1d78e52023bdfbebf76949c550db8bb9411dd), [`47b358a`](https://github.com/Effect-TS/effect/commit/47b358a4b85d22973a7957abd1a859f65b75b7da), [`45ffa72`](https://github.com/Effect-TS/effect/commit/45ffa7221259c7e4be9797b90f6ca569fe7ec8a3), [`6232650`](https://github.com/Effect-TS/effect/commit/62326505a74fcd8fe27c8e2064128d2dea484ecc), [`4b73e1b`](https://github.com/Effect-TS/effect/commit/4b73e1bd5576346d7939be8915f3053e428d1c0e), [`284050c`](https://github.com/Effect-TS/effect/commit/284050c2aa6e78eceb08e5eff1803501cc80a658), [`c85fc0b`](https://github.com/Effect-TS/effect/commit/c85fc0b8f2195780929d6fc8ecdc084faf18736c), [`7999b07`](https://github.com/Effect-TS/effect/commit/7999b078950fda15e235db1e61e8db5eb0632528), [`7d455f5`](https://github.com/Effect-TS/effect/commit/7d455f51acf2f0e9f710f298e14e4d81179a9259), [`d681c2e`](https://github.com/Effect-TS/effect/commit/d681c2eb1bb07d782540286368e8b42533df3063), [`84d2a47`](https://github.com/Effect-TS/effect/commit/84d2a470ecc0fcad7f1d1e7a50464e02af0e6972), [`ed74b18`](https://github.com/Effect-TS/effect/commit/ed74b18b95b0797808f2f7928f413f1cf2279d77), [`2245997`](https://github.com/Effect-TS/effect/commit/2245997ad2214591b7d9674777595092926743cf), [`d386979`](https://github.com/Effect-TS/effect/commit/d386979fedbdd433d28816f9cc74675a9af45189), [`fc9fedf`](https://github.com/Effect-TS/effect/commit/fc9fedf59fc26a8bc6e45c3860a921f3b7e0f971), [`7750dbe`](https://github.com/Effect-TS/effect/commit/7750dbe0d5b9929c6d92b0365b42784b993ea2a6), [`fc91af6`](https://github.com/Effect-TS/effect/commit/fc91af659d0682659e6347eede288f90f618acc0), [`39b9738`](https://github.com/Effect-TS/effect/commit/39b9738727ab4d87298fd640edd138f352c70ee3), [`88093b5`](https://github.com/Effect-TS/effect/commit/88093b57951a1b597599fe4772ab690b36a95818), [`d14c463`](https://github.com/Effect-TS/effect/commit/d14c4631ac82911a8ca0b6c24c14ecb95863ee6b), [`d473bd3`](https://github.com/Effect-TS/effect/commit/d473bd3ee966a3a0c245d0e3c752fd91368b8ff6), [`84864bc`](https://github.com/Effect-TS/effect/commit/84864bc30c9e92a1226f65bb78b0641a7e0acea2), [`e2ae724`](https://github.com/Effect-TS/effect/commit/e2ae72481e755a454f28ce45e90f43b857b7c6e5), [`f921ed3`](https://github.com/Effect-TS/effect/commit/f921ed305c63415dc468e94d9319cc3237119138), [`1df933d`](https://github.com/Effect-TS/effect/commit/1df933d1dee2e09a25f9c27214b31aa94fa9d020), [`829aff9`](https://github.com/Effect-TS/effect/commit/829aff97d71d7900b7765ba2f63e33874b898cdc), [`aa0aba3`](https://github.com/Effect-TS/effect/commit/aa0aba383382ca1b99fbe618dceba63c3a5a6a53), [`a71140f`](https://github.com/Effect-TS/effect/commit/a71140fbe79809aa5311a55b1c6908905dc2df26), [`0276a27`](https://github.com/Effect-TS/effect/commit/0276a27ddbb15d163daf56f059ca9f62c4fd9939), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`c86c999`](https://github.com/Effect-TS/effect/commit/c86c99939e1628813a2f92f0906cfbbe924504e8), [`975f758`](https://github.com/Effect-TS/effect/commit/975f7586f3dcda5743d6570a740013a9fbdce9a7), [`2a3a478`](https://github.com/Effect-TS/effect/commit/2a3a4783bcd2d4e91e3505c7a6c1e398fea89595), [`1e6e206`](https://github.com/Effect-TS/effect/commit/1e6e206f6327a114f55a541775f465c0bc9fbf3e), [`fa6027b`](https://github.com/Effect-TS/effect/commit/fa6027b16e65376078e526fa33116ac0cc82485b), [`7616f73`](https://github.com/Effect-TS/effect/commit/7616f73c16bd45a3a3e2202d3f81c6d1cc63f4e0), [`fc668b6`](https://github.com/Effect-TS/effect/commit/fc668b61e9755545a47582dee6c4425d5652cbb6), [`d425c8c`](https://github.com/Effect-TS/effect/commit/d425c8c1319519e861714bcb9d055b63a9434fad), [`ce120f4`](https://github.com/Effect-TS/effect/commit/ce120f474b87d8a5a787dd7f848177ecd3b960fc), [`248201f`](https://github.com/Effect-TS/effect/commit/248201f7846d855685a4dc1b2f01968981706d69), [`e80d397`](https://github.com/Effect-TS/effect/commit/e80d3972fcca4e986b6497d67406f45ee1be540d), [`f1a941d`](https://github.com/Effect-TS/effect/commit/f1a941d52c2f71973e998883869e4e9ef510abf2), [`73bc3a1`](https://github.com/Effect-TS/effect/commit/73bc3a13b6e1b7a060e6bcd19ae7796443211a80), [`b628bb1`](https://github.com/Effect-TS/effect/commit/b628bb1240a09df2244c3ff6c587f978feb25225), [`6e3ae7b`](https://github.com/Effect-TS/effect/commit/6e3ae7b6359bda08cea74b761fd54224d7af45e2), [`e891247`](https://github.com/Effect-TS/effect/commit/e8912473bd4128714a0f8cb04621f3e12f62a910), [`53e6c73`](https://github.com/Effect-TS/effect/commit/53e6c7311b948a62e93e57f6daee66a8ec24b030), [`1579d6f`](https://github.com/Effect-TS/effect/commit/1579d6fd063e444e4fcdfd08ff194c7485203f9c), [`d3c6b73`](https://github.com/Effect-TS/effect/commit/d3c6b73cc4b78f22fdb2e25c8bebda027551540b), [`4a59c6a`](https://github.com/Effect-TS/effect/commit/4a59c6a559e69d399b00c6acd48c8e1977e34875), [`145d8e1`](https://github.com/Effect-TS/effect/commit/145d8e1013220425b8edf34f7011c73f73e1cdcf), [`f74282c`](https://github.com/Effect-TS/effect/commit/f74282cb08de808b678ce67687acbcd61939d2f6), [`0a38623`](https://github.com/Effect-TS/effect/commit/0a3862349ebfbc41746aa0de5e682992719c1da6), [`0a08ae0`](https://github.com/Effect-TS/effect/commit/0a08ae0626f8124779ec2e32ae2088d28db157c6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`d60c5d4`](https://github.com/Effect-TS/effect/commit/d60c5d40b5954ea1557aef1502b4b87f98bbf134), [`07ffd25`](https://github.com/Effect-TS/effect/commit/07ffd25bd0e82c9e6b72b8e669833fb4531a9da3), [`9b517ad`](https://github.com/Effect-TS/effect/commit/9b517ad28a0c2f213693bcab408658c40e3b6d9b), [`604b1c1`](https://github.com/Effect-TS/effect/commit/604b1c109d6c9e654b9b7fcc98d6b80361400325), [`ccc2e02`](https://github.com/Effect-TS/effect/commit/ccc2e02def5965f5535ca97fac2051f4387c4e13), [`14d810a`](https://github.com/Effect-TS/effect/commit/14d810a726caad8a7b0d42f04a6876f25fbcc005), [`a9d1ee3`](https://github.com/Effect-TS/effect/commit/a9d1ee3d4d51e97ea33440fe6100935d5fd5aada), [`a31adbe`](https://github.com/Effect-TS/effect/commit/a31adbefd6c04c70536de11433edba9edd1f5013), [`7245f87`](https://github.com/Effect-TS/effect/commit/7245f87ace09806d15893df8f0bac8f0b9b817db), [`3a0828b`](https://github.com/Effect-TS/effect/commit/3a0828b058abb4b4646bd880458b4e636d77d1c3), [`cd83544`](https://github.com/Effect-TS/effect/commit/cd83544dd594b99e8ea0d7a7645c086f5861769b), [`6550a07`](https://github.com/Effect-TS/effect/commit/6550a074c5c501d8a95dcd354f17e9785a1ae1eb), [`6f090d4`](https://github.com/Effect-TS/effect/commit/6f090d4c2032cc35f7f9a9eac900d0329b8ac07e), [`b505c0d`](https://github.com/Effect-TS/effect/commit/b505c0db03e86eb126237690a5867f457eb4237f), [`186dd49`](https://github.com/Effect-TS/effect/commit/186dd4914084fa346a870ce9c637b9c2a6cc8100), [`9f37e58`](https://github.com/Effect-TS/effect/commit/9f37e58d00e06150d96af48fffe780947ca0d519), [`4446451`](https://github.com/Effect-TS/effect/commit/44464510114773e1ebd1abb6efffa382cf69998e), [`58be972`](https://github.com/Effect-TS/effect/commit/58be97238ee1f6cd26f6a3a645d1df97a091a224), [`1320075`](https://github.com/Effect-TS/effect/commit/1320075fd4ed381f60f6f37f280dbec17cb29b81), [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80), [`f7490d4`](https://github.com/Effect-TS/effect/commit/f7490d49c75b99118c6c42549511ffa166459f59), [`c8ea602`](https://github.com/Effect-TS/effect/commit/c8ea60231ad69e23c0bf319f6f6b3ff28fc45e4b), [`62d82f4`](https://github.com/Effect-TS/effect/commit/62d82f4919c72d8cc0cfd2b704ed6900f69ad9a8), [`a2c9e7c`](https://github.com/Effect-TS/effect/commit/a2c9e7c17a4118b74a1eab3332042dafe0bb7318), [`97dd022`](https://github.com/Effect-TS/effect/commit/97dd022fe7fbd1696dd60f3319eae594abdd3760), [`f984ee8`](https://github.com/Effect-TS/effect/commit/f984ee82d255c7958eeed6f0dd08de1b88cbbfd6), [`f1b2910`](https://github.com/Effect-TS/effect/commit/f1b291027c3055f801c2f288dda21ba84c361956), [`a29e05a`](https://github.com/Effect-TS/effect/commit/a29e05a907053a8617763b4e7408e1f012b59049), [`1aa1d8b`](https://github.com/Effect-TS/effect/commit/1aa1d8bd0f2c2d429938657927f34f8ff0f940a0), [`bb99734`](https://github.com/Effect-TS/effect/commit/bb99734ef87bcc377c628bc4072aa856ce5f7ca6), [`222e7ca`](https://github.com/Effect-TS/effect/commit/222e7caa4bb0da6d81a4eabf25dffc181c20f6a3), [`797c9e3`](https://github.com/Effect-TS/effect/commit/797c9e3c1f2e66db1570cf6aa5c4a15e94507bf0), [`b4d5398`](https://github.com/Effect-TS/effect/commit/b4d5398598c04a84054a55873c943d587880058d), [`c8349ed`](https://github.com/Effect-TS/effect/commit/c8349ede1a2add807e9ec9c37612b3a39bf284f8), [`8426e5f`](https://github.com/Effect-TS/effect/commit/8426e5f7ad11f15e39565cf8ba0874bf974f5085), [`26e0085`](https://github.com/Effect-TS/effect/commit/26e0085d098f6e526c085c357b8d82ee068b6518), [`1a86166`](https://github.com/Effect-TS/effect/commit/1a86166f102b517ee4d89dca0095e28f738eee10), [`0856631`](https://github.com/Effect-TS/effect/commit/0856631a89d8f72d206c366905ddac75468a8e85), [`0af0985`](https://github.com/Effect-TS/effect/commit/0af0985d5c132b33e14a2df59e0567d7ba2a1710), [`bc582c9`](https://github.com/Effect-TS/effect/commit/bc582c94313cbb89d87b3844ffffabebbb4ffa83), [`2c63f1e`](https://github.com/Effect-TS/effect/commit/2c63f1e07e375b7958e14929c6a4b358c819a9d0), [`0d98213`](https://github.com/Effect-TS/effect/commit/0d9821346453d44696258103643d5482e63638b4), [`ca6f0dc`](https://github.com/Effect-TS/effect/commit/ca6f0dc4fcea564b351bf505df015d16a8b965ed), [`d8cc9ed`](https://github.com/Effect-TS/effect/commit/d8cc9ed80e1df9c056a46ba62df01f7fc0f531c3), [`42fd969`](https://github.com/Effect-TS/effect/commit/42fd969b45025eed6d60cb87b97c5ff88d187671), [`5641ad3`](https://github.com/Effect-TS/effect/commit/5641ad333a88bb9e56baf886006682678391be0a), [`629870d`](https://github.com/Effect-TS/effect/commit/629870d461b36d3dacb30c8d203b8d20d4beaba0), [`d592c14`](https://github.com/Effect-TS/effect/commit/d592c1492ad895ea72537e10387b735f3ba0e7de), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`53909a9`](https://github.com/Effect-TS/effect/commit/53909a9bf14b29725a08dcccde02bee8e95a6b14), [`78a4269`](https://github.com/Effect-TS/effect/commit/78a42697f6085c87c09b18e8095bb7541bd65261), [`06c6307`](https://github.com/Effect-TS/effect/commit/06c6307d3d5538bc398c67a44239bc3d52ab1a4d), [`0847c41`](https://github.com/Effect-TS/effect/commit/0847c41e2472a347cd8f77d460d3cbdfecd4adf0), [`5a77084`](https://github.com/Effect-TS/effect/commit/5a77084651bc58617b9988606473cf9ba5ca0032), [`96f99b3`](https://github.com/Effect-TS/effect/commit/96f99b325da32bfa535e826d89d838d0edec0dea), [`7bb8781`](https://github.com/Effect-TS/effect/commit/7bb878176b8ad20cf0e671b570caf72e194f9515), [`4907e9b`](https://github.com/Effect-TS/effect/commit/4907e9b2470894ddaf0cbdec2612b59f04a51dbe), [`2a30248`](https://github.com/Effect-TS/effect/commit/2a30248b6eb739f22403456209bc468f2f4ef26a), [`8364ddd`](https://github.com/Effect-TS/effect/commit/8364ddda233d35ee472cc2356312317a60a148a2), [`ad67d8c`](https://github.com/Effect-TS/effect/commit/ad67d8c5a1b195ff5468a9f2e963c59aa0b3166f), [`ec0c087`](https://github.com/Effect-TS/effect/commit/ec0c0875262fb81c7992f5023776b8fa894c49b6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`a2c1ce6`](https://github.com/Effect-TS/effect/commit/a2c1ce62f04214d98d848f4cfe33d677e1a87d39), [`91e9af0`](https://github.com/Effect-TS/effect/commit/91e9af0d5f138933d3a131ca1cfbc52d3bc55481), [`7bd3f34`](https://github.com/Effect-TS/effect/commit/7bd3f34385a06157b249a351e32ffdcbe1cf2c37), [`ef16581`](https://github.com/Effect-TS/effect/commit/ef165814eae0a199fdca5b3c2839bb4fcdfd5b5d), [`1693a87`](https://github.com/Effect-TS/effect/commit/1693a877324273f5a23fa7d7fc892f4ee12f20c2), [`df3fc47`](https://github.com/Effect-TS/effect/commit/df3fc47609eda7843dae2b557c8fe834299e26b4), [`e11be41`](https://github.com/Effect-TS/effect/commit/e11be41dfc73fa6b5643ab9af7df18d6907927d7), [`2e39e8b`](https://github.com/Effect-TS/effect/commit/2e39e8bafef7d3fee9f3b324b7d50cc064376667), [`11c5ee7`](https://github.com/Effect-TS/effect/commit/11c5ee7202acd3bb789d0a8ce78e6c4523071553), [`1742d2f`](https://github.com/Effect-TS/effect/commit/1742d2f48441cb7b1a8d9aca3a8e6f0acbc35a96), [`8efc70e`](https://github.com/Effect-TS/effect/commit/8efc70ea16bee43fd9adde2871cf82a8131b80a0), [`9642776`](https://github.com/Effect-TS/effect/commit/964277661423f398f777ec2eab4b3ecc0b53f53b), [`6680828`](https://github.com/Effect-TS/effect/commit/668082885f52672e4c7fad4e0fe2bfa0e69703e2), [`c34edcb`](https://github.com/Effect-TS/effect/commit/c34edcb6d7a6151d53e19828c92abf7dd8450c2a), [`7b2c5bd`](https://github.com/Effect-TS/effect/commit/7b2c5bd3c394fa4751fb229394ac9c03e401f441), [`1a2ccee`](https://github.com/Effect-TS/effect/commit/1a2ccee2bbb93514dc66e0a9cdeb52a82172ab0e), [`db995df`](https://github.com/Effect-TS/effect/commit/db995df19b0381b64cc39b50aa28d7bd8f2c1294), [`af0ccdd`](https://github.com/Effect-TS/effect/commit/af0ccdd13aa8917bce7161b8bc257fbe51c6a6a5), [`addeaea`](https://github.com/Effect-TS/effect/commit/addeaea0b0abe0dc24ca1d4d0fd194a0e458be70), [`7704034`](https://github.com/Effect-TS/effect/commit/770403411bcd8befef0552a0e40066abc417f6ea), [`e72b12f`](https://github.com/Effect-TS/effect/commit/e72b12fc305710550bc6dcb978e92de8abff88cd), [`310dd9c`](https://github.com/Effect-TS/effect/commit/310dd9ce9681e97f558bb042adb35f6040ab81e3), [`1c2afc1`](https://github.com/Effect-TS/effect/commit/1c2afc15bbf4ce5a91d24da99bc275b5bd744e77), [`44f44ca`](https://github.com/Effect-TS/effect/commit/44f44cae65249801c082c3a3848016eeeac5eb24), [`f43b9d6`](https://github.com/Effect-TS/effect/commit/f43b9d6d99cb591c0377bb1474121827201627c9), [`56e72b3`](https://github.com/Effect-TS/effect/commit/56e72b38c01bbe6ec921ad151f83b7849444f219), [`50ef80e`](https://github.com/Effect-TS/effect/commit/50ef80e251b85b5ee63aed06fe0244b5433ec2fa), [`fc3b718`](https://github.com/Effect-TS/effect/commit/fc3b7187114a31cbb21a250f55fda601bb3edcf0), [`ee336d8`](https://github.com/Effect-TS/effect/commit/ee336d80b4024b51654e7a2b8d869d244153fcdf), [`d12f922`](https://github.com/Effect-TS/effect/commit/d12f922853eff2310dbe9fc9b31562ad81b38435), [`46d8310`](https://github.com/Effect-TS/effect/commit/46d83101e8408c0428d9a8760e06e23a69ad6070), [`59812fd`](https://github.com/Effect-TS/effect/commit/59812fd221cde16fc77c03d326c6a2cf8fbec6b9), [`f9d0dec`](https://github.com/Effect-TS/effect/commit/f9d0dec12c5f229f05d7a74d532a85c6bbffefbf), [`4372c79`](https://github.com/Effect-TS/effect/commit/4372c79a32fcecc7b6b284d49a215b2c4fef9d77), [`81485ef`](https://github.com/Effect-TS/effect/commit/81485ef0288a3321e75d7d4ae32b7e0e06b6f86b), [`bd393d6`](https://github.com/Effect-TS/effect/commit/bd393d63c19bdd0ab212d95576cec89051c8501c), [`0c95c04`](https://github.com/Effect-TS/effect/commit/0c95c04b41c2d5d09e4564fd997572207fd72595)]: + - effect@4.0.0-rc.113 + ## 4.0.0-rc.112 ### Patch Changes diff --git a/repos/effect/packages/ai/openai/codegen.yaml b/repos/effect/packages/ai/openai/codegen.yaml index 05724087a..c8384791a 100644 --- a/repos/effect/packages/ai/openai/codegen.yaml +++ b/repos/effect/packages/ai/openai/codegen.yaml @@ -17,6 +17,8 @@ patches: - '[{"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"}]}}]' - '[{"op":"add","path":"/components/schemas/OpenAIFile/properties/status_details/nullable","value":true}]' + # Remove when the upstream schema includes named API sources: https://github.com/openai/openai-openapi/issues/573 + - '[{"op":"replace","path":"/components/schemas/WebSearchActionSearch/properties/sources/items","value":{"anyOf":[{"type":"object","title":"Web search source","description":"A source used in the search.\n","properties":{"type":{"type":"string","enum":["url"],"description":"The type of source. Always `url`.\n","x-stainless-const":true},"url":{"type":"string","format":"uri","description":"The URL of the source.\n"}},"required":["type","url"]},{"type":"object","title":"Web search API source","description":"An API source used in the search.\n","properties":{"type":{"type":"string","enum":["api"],"description":"The type of source. Always `api`.\n","x-stainless-const":true},"name":{"type":"string","description":"The name of the API source, such as `oai-weather`, `oai-sports`, or `oai-finance`.\n"}},"required":["type","name"]}]}}]' # Add missing keepalive stream event support - '[{"op":"add","path":"/components/schemas/ResponseKeepAliveEvent","value":{"type":"object","title":"Keep alive","description":"A keepalive event emitted during long-running response streams.","properties":{"type":{"type":"string","enum":["keepalive"],"description":"The type of the keepalive event. Always `keepalive`.","default":"keepalive","x-stainless-const":true},"sequence_number":{"type":"integer","description":"The sequence number of this keepalive event."}},"required":["type","sequence_number"]}}]' - '[{"op":"add","path":"/components/schemas/ResponseStreamEvent/anyOf/-","value":{"$ref":"#/components/schemas/ResponseKeepAliveEvent"}}]' diff --git a/repos/effect/packages/ai/openai/package.json b/repos/effect/packages/ai/openai/package.json index 52e041041..000043f8f 100644 --- a/repos/effect/packages/ai/openai/package.json +++ b/repos/effect/packages/ai/openai/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai", - "version": "4.0.0-rc.112", + "version": "4.0.0-rc.117", "type": "module", "license": "MIT", "description": "An OpenAI provider integration for Effect AI SDK", diff --git a/repos/effect/packages/ai/openai/src/Generated.ts b/repos/effect/packages/ai/openai/src/Generated.ts index d51b31244..5df29a71f 100644 --- a/repos/effect/packages/ai/openai/src/Generated.ts +++ b/repos/effect/packages/ai/openai/src/Generated.ts @@ -8411,7 +8411,9 @@ export type WebSearchToolCall = { readonly "type": "search" readonly "query"?: string readonly "queries"?: ReadonlyArray - readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "sources"?: ReadonlyArray< + { readonly "type": "url"; readonly "url": string } | { readonly "type": "api"; readonly "name": string } + > } | { readonly "type": "open_page"; readonly "url"?: string | null } | { readonly "type": "find_in_page"; readonly "url": string; readonly "pattern": string } @@ -8436,10 +8438,18 @@ export const WebSearchToolCall = Schema.Struct({ ), "sources": Schema.optionalKey( Schema.Array( - Schema.Struct({ - "type": Schema.Literal("url").annotate({ "description": "The type of source. Always `url`.\n" }), - "url": Schema.String.annotate({ "description": "The URL of the source.\n", "format": "uri" }) - }).annotate({ "title": "Web search source", "description": "A source used in the search.\n" }) + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("url").annotate({ "description": "The type of source. Always `url`.\n" }), + "url": Schema.String.annotate({ "description": "The URL of the source.\n", "format": "uri" }) + }).annotate({ "title": "Web search source", "description": "A source used in the search.\n" }), + Schema.Struct({ + "type": Schema.Literal("api").annotate({ "description": "The type of source. Always `api`.\n" }), + "name": Schema.String.annotate({ + "description": "The name of the API source, such as `oai-weather`, `oai-sports`, or `oai-finance`.\n" + }) + }).annotate({ "title": "Web search API source", "description": "An API source used in the search.\n" }) + ]) ).annotate({ "title": "Web search sources", "description": "The sources used in the search.\n" }) ) }).annotate({ @@ -22773,7 +22783,9 @@ export type InputItem = readonly "type": "search" readonly "query"?: string readonly "queries"?: ReadonlyArray - readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "sources"?: ReadonlyArray< + { readonly "type": "url"; readonly "url": string } | { readonly "type": "api"; readonly "name": string } + > } | { readonly "type": "open_page"; readonly "url"?: string | null } | { readonly "type": "find_in_page"; readonly "url": string; readonly "pattern": string } @@ -23067,10 +23079,19 @@ export const InputItem = Schema.Union([ ), "sources": Schema.optionalKey( Schema.Array( - Schema.Struct({ - "type": Schema.Literal("url").annotate({ "description": "The type of source. Always `url`.\n" }), - "url": Schema.String.annotate({ "description": "The URL of the source.\n", "format": "uri" }) - }).annotate({ "title": "Web search source", "description": "A source used in the search.\n" }) + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("url").annotate({ "description": "The type of source. Always `url`.\n" }), + "url": Schema.String.annotate({ "description": "The URL of the source.\n", "format": "uri" }) + }).annotate({ "title": "Web search source", "description": "A source used in the search.\n" }), + Schema.Struct({ + "type": Schema.Literal("api").annotate({ "description": "The type of source. Always `api`.\n" }), + "name": Schema.String.annotate({ + "description": + "The name of the API source, such as `oai-weather`, `oai-sports`, or `oai-finance`.\n" + }) + }).annotate({ "title": "Web search API source", "description": "An API source used in the search.\n" }) + ]) ).annotate({ "title": "Web search sources", "description": "The sources used in the search.\n" }) ) }).annotate({ diff --git a/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts b/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts index fa466ca79..6b08fff02 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts +++ b/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts @@ -101,7 +101,7 @@ export const model = ( * * **When to use** * - * Use to construct the `EmbeddingModel.Service` effectfully when + * Use to construct the `EmbeddingModel` effectfully when * `OpenAiClient` is already available in the environment. * * **Details** @@ -127,7 +127,7 @@ export const model = ( export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { readonly model: (string & {}) | Model readonly config?: Omit | undefined -}): Effect.fn.Return { +}): Effect.fn.Return { const client = yield* OpenAiClient const makeConfig = Effect.contextWith((services: Context.Context) => diff --git a/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts b/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts index 4addf9f5b..3b3d11cb0 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts +++ b/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -19,7 +19,6 @@ 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" @@ -40,8 +39,6 @@ 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] @@ -615,7 +612,7 @@ export const model = ( export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { readonly model: (string & {}) | Model readonly config?: Omit | undefined -}): Effect.fn.Return { +}): Effect.fn.Return { const client = yield* OpenAiClient const makeConfig = Effect.gen(function*() { @@ -881,15 +878,14 @@ const prepareMessages = Effect.fnUntraced( if (typeof part.data === "string" && isFileId(part.data, config)) { content.push({ type: "input_image", file_id: part.data, detail }) - } - - if (part.data instanceof URL) { - content.push({ type: "input_image", image_url: part.data.toString(), detail }) - } - - if (part.data instanceof Uint8Array) { - const base64 = Encoding.encodeBase64(part.data) - const imageUrl = `data:${mediaType};base64,${base64}` + } else { + const imageUrl = part.data instanceof URL + ? part.data.toString() + : part.data instanceof Uint8Array + ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}` + : /^(data:|https?:\/\/)/i.test(part.data) + ? part.data + : `data:${mediaType};base64,${part.data}` content.push({ type: "input_image", image_url: imageUrl, detail }) } } else if (part.mediaType === "application/pdf") { @@ -1045,7 +1041,6 @@ const prepareMessages = Effect.fnUntraced( method: "prepareMessages", reason: new AiError.ToolParameterValidationError({ toolName: "local_shell", - toolParams: part.params as Schema.Json, description: error.message }) }) @@ -1071,7 +1066,6 @@ const prepareMessages = Effect.fnUntraced( method: "prepareMessages", reason: new AiError.ToolParameterValidationError({ toolName: "shell", - toolParams: part.params as Schema.Json, description: error.message }) }) @@ -1199,7 +1193,7 @@ const prepareMessages = Effect.fnUntraced( messages.push({ type: "function_call_output", call_id: part.id, - output: JSON.stringify(part.result), + output: typeof part.result === "string" ? part.result : JSON.stringify(part.result), ...(Predicate.isNotNull(status) ? { status } : {}) }) } @@ -1343,19 +1337,20 @@ const makeResponse = Effect.fnUntraced( case "code_interpreter_call": { const toolName = toolNameMapper.getCustomName("code_interpreter") + const status = part.status ?? "completed" parts.push({ type: "tool-call", id: part.id, name: toolName, - params: { code: part.code, container_id: part.container_id }, + params: { code: part.code ?? null, container_id: part.container_id }, providerExecuted: true }) parts.push({ type: "tool-result", id: part.id, name: toolName, - isFailure: false, - result: { outputs: part.outputs }, + isFailure: status !== "completed", + result: { status, outputs: part.outputs ?? null }, providerExecuted: true }) break @@ -1374,7 +1369,7 @@ const makeResponse = Effect.fnUntraced( type: "tool-result", id: part.id, name: toolName, - isFailure: false, + isFailure: part.status !== "completed", result: { status: part.status, queries: part.queries, @@ -1398,7 +1393,6 @@ const makeResponse = Effect.fnUntraced( method: "makeResponse", reason: new AiError.ToolParameterValidationError({ toolName, - toolParams: {}, description: `Faled to securely JSON parse tool parameters: ${cause}` }) }) @@ -1418,6 +1412,7 @@ const makeResponse = Effect.fnUntraced( case "image_generation_call": { const toolName = toolNameMapper.getCustomName("image_generation") + const status = part.status ?? "completed" parts.push({ type: "tool-call", id: part.id, @@ -1429,8 +1424,10 @@ const makeResponse = Effect.fnUntraced( type: "tool-result", id: part.id, name: toolName, - isFailure: false, - result: { result: part.result } + isFailure: status !== "completed", + result: status === "completed" + ? { result: part.result } + : { status, result: part.result } }) break } @@ -1470,7 +1467,7 @@ const makeResponse = Effect.fnUntraced( type: "tool-result", id: toolId, name: toolName, - isFailure: false, + isFailure: Predicate.isNotNullish(part.error), providerExecuted: true, result: { type: "mcp_call", @@ -1668,7 +1665,7 @@ const makeResponse = Effect.fnUntraced( type: "tool-result", id: part.id, name: toolName, - isFailure: false, + isFailure: part.status !== "completed", result: { action: part.action, status: part.status }, providerExecuted: true }) @@ -1764,9 +1761,39 @@ const makeStreamResponse = Effect.fnUntraced( } readonly codeInterpreter?: { readonly containerId: string + hasCode: boolean + streamedCode: string } }> = {} + const finishCodeInterpreterCall = ( + outputIndex: number, + code: string | null, + parts: Array + ) => { + const toolCall = activeToolCalls[outputIndex] + if (Predicate.isUndefined(toolCall?.codeInterpreter)) { + return + } + parts.push({ + type: "tool-params-delta", + id: toolCall.id, + delta: toolCall.codeInterpreter.hasCode ? "\"}" : `${JSON.stringify(code)}}` + }) + parts.push({ type: "tool-params-end", id: toolCall.id }) + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolCall.name, + params: { + code: toolCall.codeInterpreter.hasCode ? toolCall.codeInterpreter.streamedCode : code, + container_id: toolCall.codeInterpreter.containerId + }, + providerExecuted: true + }) + delete activeToolCalls[outputIndex] + } + const webSearchTool = options.tools.find((tool) => Tool.isProviderDefined(tool) && (tool.name === "OpenAiWebSearch" || @@ -1878,7 +1905,7 @@ const makeStreamResponse = Effect.fnUntraced( activeToolCalls[event.output_index] = { id: event.item.id, name: toolName, - codeInterpreter: { containerId: event.item.container_id } + codeInterpreter: { containerId: event.item.container_id, hasCode: false, streamedCode: "" } } parts.push({ type: "tool-params-start", @@ -1889,7 +1916,7 @@ const makeStreamResponse = Effect.fnUntraced( parts.push({ type: "tool-params-delta", id: event.item.id, - delta: `{"containerId":"${event.item.container_id}","code":"` + delta: `{"container_id":${JSON.stringify(event.item.container_id)},"code":` }) break } @@ -2072,14 +2099,15 @@ const makeStreamResponse = Effect.fnUntraced( } case "code_interpreter_call": { - delete activeToolCalls[event.output_index] + finishCodeInterpreterCall(event.output_index, event.item.code ?? null, parts) const toolName = toolNameMapper.getCustomName("code_interpreter") + const status = event.item.status ?? "completed" parts.push({ type: "tool-result", id: event.item.id, name: toolName, - isFailure: false, - result: { outputs: event.item.outputs }, + isFailure: status !== "completed", + result: { status, outputs: event.item.outputs ?? null }, providerExecuted: true }) break @@ -2112,15 +2140,16 @@ const makeStreamResponse = Effect.fnUntraced( case "file_search_call": { delete activeToolCalls[event.output_index] const toolName = toolNameMapper.getCustomName("file_search") - const results = Predicate.isNotNullish(event.item.results) - ? { results: event.item.results } - : undefined parts.push({ type: "tool-result", id: event.item.id, name: toolName, - isFailure: false, - result: { ...results, status: event.item.status, queries: event.item.queries }, + isFailure: event.item.status !== "completed", + result: { + status: event.item.status, + queries: event.item.queries, + results: event.item.results ?? null + }, providerExecuted: true }) break @@ -2147,7 +2176,6 @@ const makeStreamResponse = Effect.fnUntraced( method: "makeStreamResponse", reason: new AiError.ToolParameterValidationError({ toolName, - toolParams: {}, description: `Failed securely JSON parse tool parameters: ${cause}` }) }) @@ -2173,12 +2201,15 @@ const makeStreamResponse = Effect.fnUntraced( case "image_generation_call": { const toolName = toolNameMapper.getCustomName("image_generation") + const status = event.item.status ?? "completed" parts.push({ type: "tool-result", id: event.item.id, name: toolName, - isFailure: false, - result: { result: event.item.result }, + isFailure: status !== "completed", + result: status === "completed" + ? { result: event.item.result } + : { status, result: event.item.result }, providerExecuted: true }) break @@ -2223,7 +2254,7 @@ const makeStreamResponse = Effect.fnUntraced( type: "tool-result", id: toolId, name: toolName, - isFailure: false, + isFailure: Predicate.isNotNullish(event.item.error), providerExecuted: true, result: { type: "mcp_call", @@ -2331,7 +2362,7 @@ const makeStreamResponse = Effect.fnUntraced( type: "tool-result", id: event.item.id, name: toolName, - isFailure: false, + isFailure: event.item.status !== "completed", result: { action: event.item.action, status: event.item.status }, providerExecuted: true }) @@ -2450,7 +2481,6 @@ const makeStreamResponse = Effect.fnUntraced( method: "makeStreamResponse", reason: new AiError.ToolParameterValidationError({ toolName: toolCall.name, - toolParams: {}, description: `Failed securely JSON parse tool parameters: ${cause}` }) }) @@ -2516,37 +2546,21 @@ const makeStreamResponse = Effect.fnUntraced( case "response.code_interpreter_call_code.delta": { const toolCall = activeToolCalls[event.output_index] - if (Predicate.isNotUndefined(toolCall)) { + if (Predicate.isNotUndefined(toolCall?.codeInterpreter)) { parts.push({ type: "tool-params-delta", id: toolCall.id, - delta: InternalUtilities.escapeJSONDelta(event.delta) + delta: (toolCall.codeInterpreter.hasCode ? "" : "\"") + + InternalUtilities.escapeJSONDelta(event.delta) }) + toolCall.codeInterpreter.hasCode = true + toolCall.codeInterpreter.streamedCode += event.delta } break } case "response.code_interpreter_call_code.done": { - const toolCall = activeToolCalls[event.output_index] - if (Predicate.isNotUndefined(toolCall) && Predicate.isNotUndefined(toolCall.codeInterpreter)) { - const toolName = toolNameMapper.getCustomName("code_interpreter") - parts.push({ - type: "tool-params-delta", - id: toolCall.id, - delta: "\"}" - }) - parts.push({ type: "tool-params-end", id: toolCall.id }) - parts.push({ - type: "tool-call", - id: toolCall.id, - name: toolName, - params: { - code: event.code, - container_id: toolCall.codeInterpreter.containerId - }, - providerExecuted: true - }) - } + finishCodeInterpreterCall(event.output_index, event.code, parts) break } @@ -3104,7 +3118,6 @@ const normalizeMcpToolCall = Effect.fnUntraced(function* - ).pipe(Effect.mapError((error) => - AiError.make({ - module: "OpenAiLanguageModel", - method: "makeResponse", - reason: new AiError.ToolParameterValidationError({ - toolName, - toolParams, - description: formatIssue(error.issue) - }) - }) - )) + Schema.decodeEffect(codec)(toolParams) as Effect.Effect + ).pipe( + Effect.flatMap((decoded) => + Schema.encodeUnknownEffect(tool.parametersSchema)(decoded) as Effect.Effect + ), + Effect.orElseSucceed(() => toolParams) + ) }) diff --git a/repos/effect/packages/ai/openai/src/OpenAiSchema.ts b/repos/effect/packages/ai/openai/src/OpenAiSchema.ts index 2714c893a..073e56256 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiSchema.ts +++ b/repos/effect/packages/ai/openai/src/OpenAiSchema.ts @@ -766,10 +766,12 @@ const ApplyPatchCall = Schema.Struct({ const CodeInterpreterCall = Schema.Struct({ id: Schema.String, type: Schema.Literal("code_interpreter_call"), - code: Schema.optionalKey(Schema.String), + code: Schema.optionalKey(Schema.NullOr(Schema.String)), container_id: Schema.String, - outputs: Schema.optionalKey(Schema.Array(Schema.Unknown)), - status: Schema.optionalKey(MessageStatus) + outputs: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.Unknown))), + status: Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete", "interpreting", "failed"]) + ) }) const ComputerCall = Schema.Struct({ @@ -905,38 +907,38 @@ export type Response = typeof Response.Type const ResponseCreatedEvent = Schema.Struct({ type: Schema.Literal("response.created"), response: Response, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseCompletedEvent = Schema.Struct({ type: Schema.Literal("response.completed"), response: Response, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseIncompleteEvent = Schema.Struct({ type: Schema.Literal("response.incomplete"), response: Response, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseFailedEvent = Schema.Struct({ type: Schema.Literal("response.failed"), response: Response, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseOutputItemAddedEvent = Schema.Struct({ type: Schema.Literal("response.output_item.added"), output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), item: OutputItem }) const ResponseOutputItemDoneEvent = Schema.Struct({ type: Schema.Literal("response.output_item.done"), output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), item: OutputItem }) @@ -946,7 +948,7 @@ const ResponseOutputTextDeltaEvent = Schema.Struct({ output_index: Schema.Int, content_index: Schema.Int, delta: Schema.String, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), logprobs: Schema.optionalKey(Schema.Array(Schema.Unknown)) }) @@ -956,7 +958,7 @@ const ResponseOutputTextAnnotationAddedEvent = Schema.Struct({ output_index: Schema.Int, content_index: Schema.Int, annotation_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), annotation: Annotation }) @@ -965,7 +967,7 @@ const ResponseReasoningSummaryPartAddedEvent = Schema.Struct({ item_id: Schema.String, output_index: Schema.Int, summary_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), part: SummaryTextContent }) @@ -974,7 +976,7 @@ const ResponseReasoningSummaryPartDoneEvent = Schema.Struct({ item_id: Schema.String, output_index: Schema.Int, summary_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), part: SummaryTextContent }) @@ -984,14 +986,14 @@ const ResponseReasoningSummaryTextDeltaEvent = Schema.Struct({ output_index: Schema.Int, summary_index: Schema.Int, delta: Schema.String, - sequence_number: Schema.Int + sequence_number: Schema.optionalKey(Schema.Int) }) const ResponseFunctionCallArgumentsDeltaEvent = Schema.Struct({ type: Schema.Literal("response.function_call_arguments.delta"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), delta: Schema.String }) @@ -999,7 +1001,7 @@ const ResponseFunctionCallArgumentsDoneEvent = Schema.Struct({ type: Schema.Literal("response.function_call_arguments.done"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), arguments: Schema.String }) @@ -1007,7 +1009,7 @@ const ResponseCodeInterpreterCallCodeDeltaEvent = Schema.Struct({ type: Schema.Literal("response.code_interpreter_call_code.delta"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), delta: Schema.String }) @@ -1015,7 +1017,7 @@ const ResponseCodeInterpreterCallCodeDoneEvent = Schema.Struct({ type: Schema.Literal("response.code_interpreter_call_code.done"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), code: Schema.String }) @@ -1023,7 +1025,7 @@ const ResponseApplyPatchCallOperationDiffDeltaEvent = Schema.Struct({ type: Schema.Literal("response.apply_patch_call_operation_diff.delta"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), delta: Schema.String }) @@ -1031,7 +1033,7 @@ const ResponseApplyPatchCallOperationDiffDoneEvent = Schema.Struct({ type: Schema.Literal("response.apply_patch_call_operation_diff.done"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), delta: Schema.optionalKey(Schema.String) }) @@ -1039,7 +1041,7 @@ const ResponseImageGenerationCallPartialImageEvent = Schema.Struct({ type: Schema.Literal("response.image_generation_call.partial_image"), item_id: Schema.String, output_index: Schema.Int, - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), partial_image_b64: Schema.String }) @@ -1048,7 +1050,7 @@ const ResponseErrorEvent = Schema.Struct({ code: Schema.NullOr(Schema.String), message: Schema.String, param: Schema.NullOr(Schema.String), - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), status: Schema.optionalKey(Schema.Int) }) @@ -1060,7 +1062,7 @@ const NestedResponseErrorEvent = Schema.Struct({ message: Schema.String, param: Schema.NullOr(Schema.String) }), - sequence_number: Schema.Int, + sequence_number: Schema.optionalKey(Schema.Int), status: Schema.optionalKey(Schema.Int) }).pipe( Schema.decodeTo( diff --git a/repos/effect/packages/ai/openai/src/OpenAiTool.ts b/repos/effect/packages/ai/openai/src/OpenAiTool.ts index 7d80c57d3..89585a6b9 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiTool.ts +++ b/repos/effect/packages/ai/openai/src/OpenAiTool.ts @@ -67,9 +67,10 @@ export const ApplyPatch = Tool.providerDefined({ * * **Details** * - * The tool is configured with a `container` argument. Successful tool calls - * expose `outputs`, which may contain logs or generated images, or `null` when - * no outputs are available. + * The tool is configured with a `container` argument. Results include `status` + * and `outputs`: logs, generated images, or `null` when unavailable. A `completed` + * status indicates success; omitted statuses default to it. Other statuses + * produce failure results that preserve any outputs. * * @category tools * @since 4.0.0 @@ -86,6 +87,13 @@ export const CodeInterpreter = Tool.providerDefined({ container_id: Generated.CodeInterpreterToolCall.fields.container_id }), success: Schema.Struct({ + status: Schema.Literal("completed"), + outputs: Generated.CodeInterpreterToolCall.fields.outputs + }), + failure: Schema.Struct({ + status: Schema.Literals( + Generated.CodeInterpreterToolCall.fields.status.literals.filter((status) => status !== "completed") + ), outputs: Generated.CodeInterpreterToolCall.fields.outputs }) }) @@ -102,8 +110,10 @@ export const CodeInterpreter = Tool.providerDefined({ * **Details** * * The tool requires `vector_store_ids` and accepts optional `filters`, - * `max_num_results`, and `ranking_options`. Successful tool calls expose the - * search `status`, generated `queries`, and optional `results`. + * `max_num_results`, and `ranking_options`. Results include `queries` and a + * required `results` field containing matches or `null`. Only `completed` is + * successful; all other statuses produce failure results, preserving partial + * matches. * * @category tools * @since 4.0.0 @@ -119,9 +129,16 @@ export const FileSearch = Tool.providerDefined({ vector_store_ids: Generated.FileSearchTool.fields.vector_store_ids }), success: Schema.Struct({ - status: Generated.FileSearchToolCall.fields.status, + status: Schema.Literal("completed"), + queries: Generated.FileSearchToolCall.fields.queries, + results: Generated.FileSearchToolCall.fields.results.schema + }), + failure: Schema.Struct({ + status: Schema.Literals( + Generated.FileSearchToolCall.fields.status.literals.filter((status) => status !== "completed") + ), queries: Generated.FileSearchToolCall.fields.queries, - results: Generated.FileSearchToolCall.fields.results + results: Generated.FileSearchToolCall.fields.results.schema }) }) @@ -138,8 +155,10 @@ export const FileSearch = Tool.providerDefined({ * * The tool configures the `image_generation` provider tool, including model, * size, quality, output format, moderation, background, input-image options, - * and partial image settings. Successful tool calls expose `result` as base64 - * image data or `null`. + * and partial image settings. Successful calls expose `result` as base64 image + * data or `null`. Partial images are preliminary success results. Final results + * with a status other than `completed` are failures that include `status`. + * Omitted statuses default to `completed`. * * @category tools * @since 4.0.0 @@ -162,6 +181,12 @@ export const ImageGeneration = Tool.providerDefined({ }), success: Schema.Struct({ result: Generated.ImageGenToolCall.fields.result + }), + failure: Schema.Struct({ + status: Schema.Literals( + Generated.ImageGenToolCall.fields.status.literals.filter((status) => status !== "completed") + ), + result: Generated.ImageGenToolCall.fields.result }) }) @@ -197,6 +222,14 @@ export const LocalShell = Tool.providerDefined({ }) }) +const McpResultFields = { + type: Generated.MCPToolCall.fields.type, + name: Generated.MCPToolCall.fields.name, + arguments: Generated.MCPToolCall.fields.arguments, + output: Generated.MCPToolCall.fields.output, + server_label: Generated.MCPToolCall.fields.server_label +} + /** * Defines the OpenAI MCP tool that gives the model access to additional tools via remote * Model Context Protocol (MCP) servers. @@ -210,7 +243,8 @@ export const LocalShell = Tool.providerDefined({ * The tool accepts MCP server configuration such as allowed tools, * authorization, connector id, approval requirements, server metadata, and * server URL. Tool call results include the called tool name, arguments, output, - * error, and server label. + * and server label. Calls that report an `error` produce failure results that + * include it; successful results omit `error`. * * **Gotchas** * @@ -234,13 +268,10 @@ export const Mcp = Tool.providerDefined({ server_url: Generated.MCPTool.fields.server_url }), parameters: Schema.Unknown, - success: Schema.Struct({ - type: Generated.MCPToolCall.fields.type, - name: Generated.MCPToolCall.fields.name, - arguments: Generated.MCPToolCall.fields.arguments, - output: Generated.MCPToolCall.fields.output, - error: Generated.MCPToolCall.fields.error, - server_label: Generated.MCPToolCall.fields.server_label + success: Schema.Struct(McpResultFields), + failure: Schema.Struct({ + ...McpResultFields, + error: Schema.String }) }) @@ -274,6 +305,16 @@ export const Shell = Tool.providerDefined({ }) }) +const WebSearchSuccess = Schema.Struct({ + action: Generated.WebSearchToolCall.fields.action, + status: Schema.Literal("completed") +}) + +const WebSearchFailure = Schema.Struct({ + action: Generated.WebSearchToolCall.fields.action, + status: Schema.Literals(Generated.WebSearchToolCall.fields.status.literals.filter((status) => status !== "completed")) +}) + /** * Defines the OpenAI Web Search tool that enables the model to search the web for * information. @@ -285,7 +326,10 @@ export const Shell = Tool.providerDefined({ * **Details** * * The tool accepts optional filters, user location, and search context size. - * Successful calls expose the performed search action and status. + * Results preserve the action and status. Only `completed` is successful; + * all other statuses produce failure results. + * Narrow search sources by `type` to read `url` for URL sources or `name` + * for API sources. * * @see {@link WebSearchPreview} for the preview web search provider tool * @@ -304,10 +348,8 @@ export const WebSearch = Tool.providerDefined({ parameters: Schema.Struct({ action: Generated.WebSearchToolCall.fields.action }), - success: Schema.Struct({ - action: Generated.WebSearchToolCall.fields.action, - status: Generated.WebSearchToolCall.fields.status - }) + success: WebSearchSuccess, + failure: WebSearchFailure }) /** @@ -319,8 +361,11 @@ export const WebSearch = Tool.providerDefined({ * * **Details** * - * The preview tool accepts optional user location and search context size, then - * exposes the performed search action and status in successful calls. + * The preview tool accepts optional user location and search context size. + * Results preserve the action and status. Only `completed` is successful; + * all other statuses produce failure results. + * Narrow search sources by `type` to read `url` for URL sources or `name` + * for API sources. * * @see {@link WebSearch} for the stable web search provider tool * @@ -335,8 +380,6 @@ export const WebSearchPreview = Tool.providerDefined({ user_location: Generated.WebSearchPreviewTool.fields.user_location, search_context_size: Generated.WebSearchPreviewTool.fields.search_context_size }), - success: Schema.Struct({ - action: Generated.WebSearchToolCall.fields.action, - status: Generated.WebSearchToolCall.fields.status - }) + success: WebSearchSuccess, + failure: WebSearchFailure }) diff --git a/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts b/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts index c06647cba..c20107c5c 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts +++ b/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts @@ -611,6 +611,138 @@ describe("OpenAiClient", () => { ] })))) + it.effect("accepts response stream events without sequence numbers", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + const [, stream] = yield* client.createResponseStream({ + model: "gpt-4o", + input: "test" + }) + + const events = yield* Stream.runCollect(stream) + const decoded = globalThis.Array.from(events) + + assert.deepStrictEqual(decoded.map((event) => event.type), [ + "response.created", + "response.output_item.added", + "response.content_part.added", + "response.output_text.delta", + "response.reasoning_text.delta", + "response.reasoning_text.done", + "response.content_part.done", + "response.completed" + ]) + assert.deepStrictEqual(decoded[0], { + type: "response.created", + response: { + id: "resp_test123", + object: "response", + model: "gpt-4o-mini", + created_at: 1, + output: [], + error: null, + incomplete_details: null + } + }) + assert.deepStrictEqual(decoded[3], { + type: "response.output_text.delta", + output_index: 0, + item_id: "msg_123", + content_index: 0, + delta: "hello", + sequence_number: 4 + }) + assert.deepStrictEqual([decoded[2], decoded[4], decoded[5], decoded[6]], [ + { + type: "response.content_part.added", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + part: { type: "reasoning_text", text: "" } + }, + { + type: "response.reasoning_text.delta", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + delta: "thinking..." + }, + { + type: "response.reasoning_text.done", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + text: "thinking..." + }, + { + type: "response.content_part.done", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + part: { type: "reasoning_text", text: "thinking..." } + } + ]) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Sse", + events: [ + { + type: "response.created", + response: makeResponseBody({ status: "in_progress" }) + }, + { + type: "response.output_item.added", + output_index: 0, + item: { + id: "msg_123", + type: "message", + role: "assistant", + status: "in_progress", + content: [] + } + }, + { + type: "response.content_part.added", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + part: { type: "reasoning_text", text: "" } + }, + { + type: "response.output_text.delta", + output_index: 0, + item_id: "msg_123", + content_index: 0, + delta: "hello", + sequence_number: 4 + }, + { + type: "response.reasoning_text.delta", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + delta: "thinking..." + }, + { + type: "response.reasoning_text.done", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + text: "thinking..." + }, + { + type: "response.content_part.done", + output_index: 0, + item_id: "rs_tmp_123", + content_index: 0, + part: { type: "reasoning_text", text: "thinking..." } + }, + { + type: "response.completed", + response: makeResponseBody() + } + ] + })))) + it.effect("maps HTTP error before stream starts", () => Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient diff --git a/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts b/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts index 19b1e4527..7fda8b768 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts +++ b/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts @@ -1,10 +1,21 @@ import { type Generated, OpenAiClient, OpenAiLanguageModel, OpenAiSchema, OpenAiTool } from "@effect/ai-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, Response as AiResponse, Tool, Toolkit } from "effect/unstable/ai" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array, Context, Effect, Layer, Redacted, Ref, Schema, SchemaGetter, Stream } from "effect" +import { type AiError, LanguageModel, Prompt, Response as AiResponse, Tool, Toolkit } from "effect/unstable/ai" import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +const webSearchFailures = [ + { tool: OpenAiTool.WebSearch({}), status: "failed" }, + { tool: OpenAiTool.WebSearchPreview({}), status: "searching" } +] as const + +const fileSearchOutcomes = [ + { status: "completed", isFailure: false, results: undefined }, + { status: "failed", isFailure: true, results: null }, + { status: "incomplete", isFailure: true, results: [{ file_id: "file_123", text: "Matching text" }] } +] as const + describe("OpenAiLanguageModel", () => { describe("make", () => { it.effect("sends correct model in request", () => @@ -29,6 +40,51 @@ describe("OpenAiLanguageModel", () => { }).pipe(Effect.provide(makeTestLayer({ body: { model: "ft:gpt-4o-mini:custom" as any } })))) }) + describe("web search sources", () => { + it.effect.each(["generateText", "streamText"] as const)( + "preserves URL and API sources with %s", + (method) => + Effect.gen(function*() { + const sources = [ + { type: "url", url: "https://example.com/weather" }, + { type: "api", name: "oai-weather" } + ] as const + const action = { type: "search", query: "weather in London", sources } as const + const item = makeWebSearchCall({ action }) + const event = { type: "response.output_item.done", sequence_number: 1, output_index: 0, item } + const body = method === "generateText" + ? JSON.stringify(makeDefaultResponse({ output: [item] })) + : `data: ${JSON.stringify(event)}\n\n` + const client = HttpClient.make((request) => + Effect.succeed(HttpClientResponse.fromWeb( + request, + new Response(body, { + headers: { "content-type": method === "generateText" ? "application/json" : "text/event-stream" } + }) + )) + ) + const options = { prompt: "Weather in London", toolkit: Toolkit.make(OpenAiTool.WebSearch({})) } + const parts = yield* Effect.gen(function*() { + if (method === "generateText") { + const response = yield* LanguageModel.generateText(options) + return response.content + } + return yield* LanguageModel.streamText(options).pipe(Stream.runCollect) + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-5.6")), + Effect.provide(OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") })), + Effect.provideService(HttpClient.HttpClient, client) + ) + + const calls = parts.filter((part) => part.type === "tool-call") + deepStrictEqual(calls.map((part) => part.params), [{ action }]) + + const results = parts.filter((part) => part.type === "tool-result") + deepStrictEqual(results.map((part) => part.result), [{ action, status: "completed" }]) + }) + ) + }) + describe("generateText", () => { describe("message preparation", () => { it.effect("forwards prompt cache configuration and text breakpoints", () => @@ -208,6 +264,35 @@ describe("OpenAiLanguageModel", () => { }]) }).pipe(Effect.provide(makeTestLayer()))) + it.effect("handles image strings", () => + Effect.gen(function*() { + const base64 = "iVBORw0KGgo=" + const dataUrl = `data:image/png;base64,${base64}` + const upperCaseDataUrl = `DATA:image/png;base64,${base64}` + const url = "https://example.com/image.png" + + yield* LanguageModel.generateText({ + prompt: Prompt.make([Prompt.userMessage({ + content: [base64, dataUrl, upperCaseDataUrl, url].map((data) => + Prompt.filePart({ mediaType: "image/png", data }) + ) + })]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + assert.deepStrictEqual(body.input, [{ + role: "user", + content: [ + { type: "input_image", image_url: `data:image/png;base64,${base64}`, detail: "auto" }, + { type: "input_image", image_url: dataUrl, detail: "auto" }, + { type: "input_image", image_url: upperCaseDataUrl, detail: "auto" }, + { type: "input_image", image_url: url, detail: "auto" } + ] + }]) + }).pipe(Effect.provide(makeTestLayer()))) + it.effect("handles image with custom detail level", () => Effect.gen(function*() { yield* LanguageModel.generateText({ @@ -505,6 +590,46 @@ describe("OpenAiLanguageModel", () => { strictEqual(toolOutput.output, JSON.stringify({ output: "result" })) }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + it.effect("preserves string tool results", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: [ + Prompt.toolCallPart({ + id: "call_text", + name: "TestTool", + params: { input: "test" }, + providerExecuted: false + }) + ] + }, + { + role: "tool", + content: [ + Prompt.toolResultPart({ + id: "call_text", + name: "TestTool", + isFailure: false, + result: "PLAIN_TEXT_SENTINEL\n", + providerExecuted: false + }) + ] + } + ]), + toolkit: TestToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const toolOutput = body.input.find((item: any) => item.type === "function_call_output") + + assert.isDefined(toolOutput) + strictEqual(toolOutput.output, "PLAIN_TEXT_SENTINEL\n") + }).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({})) @@ -968,6 +1093,112 @@ describe("OpenAiLanguageModel", () => { ]) )) + it.effect("routes invalid tool call params through failureMode: return without failing the effect", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(ReturnModeTool) + const handlers = toolkit.toLayer({ + ReturnModeTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(handlers) + ) + + strictEqual(result.toolResults.length, 1) + const toolResult = result.toolResults[0]! + strictEqual(toolResult.isFailure, true) + const failure = toolResult.result as AiError.AiError + strictEqual(failure._tag, "AiError") + strictEqual(failure.reason._tag, "ToolParameterValidationError") + }).pipe( + Effect.provide(makeTestLayer({ + body: { output: [makeFunctionCall("ReturnModeTool", { input: 123 })] } + })) + )) + + it.effect("converts transformed tool call params to the tool's standard encoded form", () => + Effect.gen(function*() { + const received = yield* Ref.make(undefined) + const toolkit = Toolkit.make(TransformParamsTool) + const handlers = toolkit.toLayer({ + TransformParamsTool: ({ input }) => + Ref.set(received, input).pipe( + Effect.as({ output: input * 2 }) + ) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(handlers) + ) + + const toolCall = result.toolCalls[0]! + deepStrictEqual(toolCall.params, { input: "21" }) + strictEqual(yield* Ref.get(received), 21) + strictEqual(result.toolResults[0]!.isFailure, false) + deepStrictEqual(result.toolResults[0]!.result, { output: 42 }) + }).pipe( + Effect.provide(makeTestLayer({ + body: { output: [makeFunctionCall("TransformParamsTool", { input: "21" })] } + })) + )) + + it.effect("provides parameter encoding services to provider normalization", () => + Effect.gen(function*() { + const used = yield* Ref.make(false) + const toolkit = Toolkit.make(AsymmetricParamsTool) + const handlers = toolkit.toLayer({ + AsymmetricParamsTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(handlers), + Effect.provideService(ParamEncodeService, { use: Ref.set(used, true) }) + ) + + strictEqual(yield* Ref.get(used), true) + strictEqual(result.toolResults[0]!.isFailure, false) + deepStrictEqual(result.toolResults[0]!.result, { output: "processed: hello" }) + }).pipe( + Effect.provide(makeTestLayer({ + body: { output: [makeFunctionCall("AsymmetricParamsTool", { input: "hello" })] } + })) + )) + + it.effect("provides parameter encoding services to provider normalization when tool call resolution is disabled", () => + Effect.gen(function*() { + const used = yield* Ref.make(false) + const toolkit = Toolkit.make(AsymmetricParamsTool) + + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit, + disableToolCallResolution: true + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provideService(ParamEncodeService, { use: Ref.set(used, true) }) + ) + + strictEqual(yield* Ref.get(used), true) + const toolCall = result.toolCalls[0]! + deepStrictEqual(toolCall.params, { input: "hello" }) + }).pipe( + Effect.provide(makeTestLayer({ + body: { output: [makeFunctionCall("AsymmetricParamsTool", { input: "hello" })] } + })) + )) + it.effect("uses canonical OpenAiMcp name for mcp_call", () => Effect.gen(function*() { const result = yield* LanguageModel.generateText({ @@ -986,6 +1217,7 @@ describe("OpenAiLanguageModel", () => { assert.isDefined(toolResult) if (toolResult?.type === "tool-result") { strictEqual(toolResult.name, "OpenAiMcp") + assertTrue(!toolResult.isFailure, "expected a successful MCP result") strictEqual(toolResult.result.name, "CheckPackage") } }).pipe(Effect.provide(makeTestLayer({ @@ -1026,6 +1258,47 @@ describe("OpenAiLanguageModel", () => { ) ) + for (const { tool, status } of webSearchFailures) { + it.effect( + `${tool.name} preserves ${status} results`, + () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Search the web", + toolkit: Toolkit.make(tool) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + const toolResult = result.toolResults[0]! + strictEqual(toolResult.isFailure, true) + deepStrictEqual(toolResult.result, { + action: { type: "search", query: "Effect TypeScript" }, + status + }) + }).pipe(Effect.provide(makeTestLayer({ + body: { output: [makeWebSearchCall({ status })] } + }))) + ) + } + + it.effect.each(fileSearchOutcomes)( + "OpenAiFileSearch reports $status results", + ({ status, isFailure, results }) => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Search the files", + toolkit: Toolkit.make(OpenAiTool.FileSearch({ vector_store_ids: ["vs_123"] })) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + const toolResult = result.toolResults[0]! + strictEqual(toolResult.isFailure, isFailure) + deepStrictEqual(toolResult.result, { + status, + queries: ["Effect TypeScript"], + results: results ?? null + }) + }).pipe(Effect.provide(makeTestLayer({ + body: { output: [makeFileSearchCall({ status, ...(results === undefined ? {} : { results }) })] } + }))) + ) + it.effect("uses canonical OpenAiMcp name for mcp_approval_request", () => Effect.gen(function*() { const result = yield* LanguageModel.generateText({ @@ -1373,6 +1646,78 @@ describe("OpenAiLanguageModel", () => { assert.isDefined(toolParamsEnd) })) + it.effect("routes invalid tool call params through failureMode: return without failing the stream", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(ReturnModeTool) + const handlers = toolkit.toLayer({ + ReturnModeTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) + }) + + const streamEvents = [ + { + type: "response.created", + sequence_number: 1, + response: makeDefaultResponse({ + id: "resp_invalid_params", + status: "in_progress", + output: [] + }) + }, + { + type: "response.output_item.added", + sequence_number: 2, + output_index: 0, + item: { + type: "function_call", + id: "fc_1", + call_id: "call_1", + name: "ReturnModeTool", + arguments: "", + status: "in_progress" + } + }, + { + type: "response.function_call_arguments.done", + sequence_number: 3, + output_index: 0, + item_id: "fc_1", + name: "ReturnModeTool", + arguments: "{\"input\":123}" + }, + { + type: "response.completed", + sequence_number: 4, + response: makeDefaultResponse({ + id: "resp_invalid_params", + status: "completed", + output: [] + }) + } + ] as unknown as ReadonlyArray + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Use the test tool", + toolkit + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer(streamEvents)), + Effect.provide(handlers) + ) + + const parts = globalThis.Array.from(partsChunk) + const toolResults = parts.filter((part) => part.type === "tool-result") + strictEqual(toolResults.length, 1) + const toolResult = toolResults[0] + if (toolResult?.type === "tool-result") { + strictEqual(toolResult.isFailure, true) + const result = toolResult.result as AiError.AiError + strictEqual(result._tag, "AiError") + strictEqual(result.reason._tag, "ToolParameterValidationError") + } + assert.isDefined(parts.find((part) => part.type === "finish")) + })) + it.effect("waits for the stable streamed web search action before emitting the tool call", () => Effect.gen(function*() { const toolkit = Toolkit.make(OpenAiTool.WebSearch({})) @@ -1426,6 +1771,63 @@ describe("OpenAiLanguageModel", () => { }) })) + for (const { tool, status } of webSearchFailures) { + it.effect( + `${tool.name} preserves ${status} results from output_item.done`, + () => + Effect.gen(function*() { + const parts = yield* LanguageModel.streamText({ + prompt: "Search the web", + toolkit: Toolkit.make(tool) + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer([{ + type: "response.output_item.done", + sequence_number: 1, + output_index: 0, + item: makeWebSearchCall({ status }) + }])) + ) + + const toolResult = parts.find((part) => part.type === "tool-result")! + strictEqual(toolResult.isFailure, true) + deepStrictEqual(toolResult.result, { + action: { type: "search", query: "Effect TypeScript" }, + status + }) + }) + ) + } + + it.effect.each(fileSearchOutcomes)( + "OpenAiFileSearch reports $status results from output_item.done", + ({ status, isFailure, results }) => + Effect.gen(function*() { + const parts = yield* LanguageModel.streamText({ + prompt: "Search the files", + toolkit: Toolkit.make(OpenAiTool.FileSearch({ vector_store_ids: ["vs_123"] })) + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer([{ + type: "response.output_item.done", + sequence_number: 1, + output_index: 0, + item: makeFileSearchCall({ status, ...(results === undefined ? {} : { results }) }) + }])) + ) + + const toolResult = parts.find((part) => part.type === "tool-result")! + strictEqual(toolResult.isFailure, isFailure) + deepStrictEqual(toolResult.result, { + status, + queries: ["Effect TypeScript"], + results: results ?? null + }) + }) + ) + it.effect("handles reasoning summary events when reasoning state is missing", () => Effect.gen(function*() { const streamEvents = [ @@ -1540,6 +1942,7 @@ describe("OpenAiLanguageModel", () => { assert.isDefined(toolResult) if (toolResult?.type === "tool-result") { strictEqual(toolResult.name, "OpenAiMcp") + assertTrue(!toolResult.isFailure, "expected a successful MCP result") strictEqual(toolResult.result.name, "CheckPackage") } })) @@ -1849,6 +2252,16 @@ const makeWebSearchCall = ( ...overrides }) +const makeFileSearchCall = ( + overrides: Partial = {} +): Generated.FileSearchToolCall => ({ + type: "file_search_call", + id: "fs_123", + status: "completed", + queries: ["Effect TypeScript"], + ...overrides +}) + const makeMcpCall = ( name: string, args: Record, @@ -1912,6 +2325,42 @@ const TestTool = Tool.make("TestTool", { success: Schema.Struct({ output: Schema.String }) }) +const ReturnModeTool = Tool.make("ReturnModeTool", { + description: "A test tool", + failureMode: "return", + parameters: Schema.Struct({ input: Schema.String }), + success: Schema.Struct({ output: Schema.String }), + failure: Schema.Struct({ error: Schema.String }) +}) + +const TransformParamsTool = Tool.make("TransformParamsTool", { + description: "A test tool", + parameters: Schema.Struct({ input: Schema.FiniteFromString }), + success: Schema.Struct({ output: Schema.Finite }) +}) + +class ParamEncodeService extends Context.Service +}>()("ParamEncodeService") {} + +const AsymmetricParam = Schema.String.pipe( + Schema.decodeTo(Schema.String, { + decode: SchemaGetter.passthrough(), + encode: SchemaGetter.transformEffect((value) => + Effect.service(ParamEncodeService).pipe( + Effect.flatMap((service) => service.use), + Effect.as(value) + ) + ) + }) +) + +const AsymmetricParamsTool = Tool.make("AsymmetricParamsTool", { + description: "A test tool", + parameters: Schema.Struct({ input: AsymmetricParam }), + success: Schema.Struct({ output: Schema.String }) +}) + const TestToolkit = Toolkit.make(TestTool) const McpToolkit = Toolkit.make(OpenAiTool.Mcp({ diff --git a/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts b/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts index 8cdd06a1b..1b97d692b 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts +++ b/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts @@ -1,4 +1,4 @@ -import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" +import { Generated, OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" import * as OpenAiSchema from "@effect/ai-openai/OpenAiSchema" import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Schema, Stream } from "effect" @@ -17,6 +17,36 @@ const makeResponse = (overrides: Record = {}) => ({ }) describe("OpenAiSchema", () => { + describe("web search sources", () => { + it("preserves API sources in response history", () => { + const item = { + type: "web_search_call", + id: "ws_123", + status: "completed", + action: { type: "search", sources: [{ type: "api", name: "oai-weather" }] } + } as const + + assert.deepStrictEqual(Schema.decodeUnknownSync(Generated.InputItem)(item), item) + }) + + it.each([ + { type: "url" }, + { type: "api" }, + { type: "api", name: null }, + { type: "unknown", name: "oai-weather" } + ])("rejects malformed search sources: %j", (source) => { + const item = { + type: "web_search_call", + id: "ws_123", + status: "completed", + action: { type: "search", sources: [source] } + } + + assert.throws(() => Schema.decodeUnknownSync(Generated.WebSearchToolCall)(item)) + assert.throws(() => Schema.decodeUnknownSync(Generated.InputItem)(item)) + }) + }) + it("accepts max reasoning effort", () => { const decoded = Schema.decodeUnknownSync(OpenAiSchema.CreateResponse)({ reasoning: { effort: "max" } diff --git a/repos/effect/packages/ai/openai/typetest/OpenAiTool.tst.ts b/repos/effect/packages/ai/openai/typetest/OpenAiTool.tst.ts new file mode 100644 index 000000000..49d018fe9 --- /dev/null +++ b/repos/effect/packages/ai/openai/typetest/OpenAiTool.tst.ts @@ -0,0 +1,50 @@ +import { type Generated, OpenAiTool } from "@effect/ai-openai" +import type { Tool } from "effect/unstable/ai" +import { describe, expect, it } from "tstyche" + +describe("OpenAiTool", () => { + it("distinguishes completed file searches from failed and unfinished searches", () => { + const fileSearch = OpenAiTool.FileSearch({ vector_store_ids: ["vs_123"] }) + + type Failure = { + readonly status: "in_progress" | "searching" | "incomplete" | "failed" + readonly queries: Generated.FileSearchToolCall["queries"] + readonly results: Exclude + } + + expect>().type.toBe() + expect>().type.toBe & { readonly status: "completed" }>() + }) + + it("distinguishes completed web searches from failed and unfinished searches", () => { + const webSearch = OpenAiTool.WebSearch({}) + + type Failure = { + readonly action: Generated.WebSearchToolCall["action"] + readonly status: "in_progress" | "searching" | "failed" + } + + expect>().type.toBe() + expect["status"]>().type.toBe<"completed">() + }) + + it("keeps search sources optional and distinguishes URL sources from named API sources", () => { + const webSearch = OpenAiTool.WebSearch({}) + const preview = OpenAiTool.WebSearchPreview({}) + type Sources = Pick< + Extract, + "sources" + > + type Expected = { + readonly sources?: ReadonlyArray< + | { readonly type: "url"; readonly url: string } + | { readonly type: "api"; readonly name: string } + > + } + + expect["action"]>>().type.toBe() + expect["action"]>>().type.toBe() + expect["action"]>>().type.toBe() + expect["action"]>>().type.toBe() + }) +}) diff --git a/repos/effect/packages/ai/openrouter/CHANGELOG.md b/repos/effect/packages/ai/openrouter/CHANGELOG.md index 6c9f47b0a..b73053ad8 100644 --- a/repos/effect/packages/ai/openrouter/CHANGELOG.md +++ b/repos/effect/packages/ai/openrouter/CHANGELOG.md @@ -1,5 +1,53 @@ # @effect/ai-openrouter +## 4.0.0-rc.117 + +### Patch Changes + +- Updated dependencies [[`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9), [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724), [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0), [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b), [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9), [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a), [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5), [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741)]: + - effect@4.0.0-rc.117 + +## 4.0.0-rc.116 + +### Patch Changes + +- [#8287](https://github.com/Effect-TS/effect/pull/8287) [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2) Thanks @tim-smart! - Add `OpenRouterDecisionModel` for OpenRouter's alpha Decisions API. + + Custom `OpenRouterClient.Service` implementations and mocks must now provide `createDecisions`. +- Updated dependencies [[`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f), [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2), [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d), [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356), [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf), [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b), [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f), [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a), [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac), [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06), [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8), [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153), [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537), [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746), [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9), [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c), [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3), [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b), [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f), [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5), [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55), [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b)]: + - effect@4.0.0-rc.116 + +## 4.0.0-rc.115 + +### Patch Changes + +- Updated dependencies [[`657254b`](https://github.com/Effect-TS/effect/commit/657254b8218628b0116497d09aaf783cb88279f3), [`f9ef0e9`](https://github.com/Effect-TS/effect/commit/f9ef0e970260b5de143392d00593e6c8b4614cff), [`4f73f9e`](https://github.com/Effect-TS/effect/commit/4f73f9e0bd9538260f0f7166e0e6abdbd507a0e8)]: + - effect@4.0.0-rc.115 + +## 4.0.0-rc.114 + +### Patch Changes + +- [#8170](https://github.com/Effect-TS/effect/pull/8170) [`b0f7be1`](https://github.com/Effect-TS/effect/commit/b0f7be17434838e2cfcda1f90f6ae299e61b2859) Thanks @JeremyFunk! - Fix negative text and uncached token usage when reasoning or cached counts exceed their parent totals. Treat these counts as disjoint, adding them to the total and retaining the parent count as text or uncached usage. + + Disjoint counts at or below their parent totals remain indistinguishable from subsets, so their totals are still undercounted. +- Updated dependencies [[`3ff4952`](https://github.com/Effect-TS/effect/commit/3ff49521afa6fe6c85c2c230f7e986f7374ef353), [`6d55555`](https://github.com/Effect-TS/effect/commit/6d55555b83701c42855758944985a6fdbf57fffe), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`d4e4ad5`](https://github.com/Effect-TS/effect/commit/d4e4ad57d06d0a30d5e8090ed951f080bcd3a3d7), [`b1988f4`](https://github.com/Effect-TS/effect/commit/b1988f496eef07482f10f4cc03ef20b9570ea0ac), [`482b7d7`](https://github.com/Effect-TS/effect/commit/482b7d7eb08ebe6bc57781414a5a9a12600b2c71), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`9941e6d`](https://github.com/Effect-TS/effect/commit/9941e6dbf800cfd3723bfb421306e04e36b5882d)]: + - effect@4.0.0-rc.114 + +## 4.0.0-rc.113 + +### Patch Changes + +- [#7588](https://github.com/Effect-TS/effect/pull/7588) [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494) Thanks @tim-smart! - Route tool call parameter validation failures through the tool's `failureMode` and drop `ToolParameterValidationError.toolParams`. + +- [#7764](https://github.com/Effect-TS/effect/pull/7764) [`62945c1`](https://github.com/Effect-TS/effect/commit/62945c1bafdf9dae2221eeda9cc705c0154c698c) Thanks @kitlangton! - Report `tool-calls` instead of `unknown` when an OpenRouter stream ends with `stop` after emitting tool calls and nonempty encrypted reasoning. + +- [#8102](https://github.com/Effect-TS/effect/pull/8102) [`98592a2`](https://github.com/Effect-TS/effect/commit/98592a2373ae3b7acd541905ec3696dfbd44453a) Thanks @tim-smart! - Exclude the provider-only `strictJsonSchema` option from outgoing OpenRouter request bodies while preserving structured-output and tool strictness settings. + +- [#7805](https://github.com/Effect-TS/effect/pull/7805) [`0b93584`](https://github.com/Effect-TS/effect/commit/0b935848082dd178db8278bff52a2cd07e5bf69a) Thanks @tim-smart! - Preserve plain-text client tool results when encoding provider requests. +- Updated dependencies [[`49e3901`](https://github.com/Effect-TS/effect/commit/49e39010ff84acbde32c50d559a2f76426cf830e), [`b945ded`](https://github.com/Effect-TS/effect/commit/b945ded23aa9a0ad88bb55aa4089680866dccf92), [`d6422f4`](https://github.com/Effect-TS/effect/commit/d6422f4104062e3e176abe92ef0e444cd4cc6bab), [`5a80204`](https://github.com/Effect-TS/effect/commit/5a802043984727b0c5a291af39d1b9bbfa8d7b8b), [`53511ef`](https://github.com/Effect-TS/effect/commit/53511efcd1c7a05d34b4c9a19c19ed317ebc5d4c), [`79ae49f`](https://github.com/Effect-TS/effect/commit/79ae49f19cab9a42c8f27ae8b104d5e39e6131fa), [`0d083ba`](https://github.com/Effect-TS/effect/commit/0d083ba26b2e1afec8d3e8d83db0d05683b6602b), [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803), [`debe8fd`](https://github.com/Effect-TS/effect/commit/debe8fdc15d00d51cf707353365d7b877123eec6), [`a8588f9`](https://github.com/Effect-TS/effect/commit/a8588f99a43dc9b85dacb14f7be5d3e1ed994d4f), [`f17eb0a`](https://github.com/Effect-TS/effect/commit/f17eb0a6f669e09647c5ded5832ea6baac596659), [`f30cbfe`](https://github.com/Effect-TS/effect/commit/f30cbfe53951128ad8124ae733f12423dbc78bd8), [`78cc9c0`](https://github.com/Effect-TS/effect/commit/78cc9c0d0f36b59f7034b2ace56a4a5c02b7a551), [`ccbdbd5`](https://github.com/Effect-TS/effect/commit/ccbdbd51e7bdb114302701fbc03021868b71956a), [`a63dcbf`](https://github.com/Effect-TS/effect/commit/a63dcbf04e5c3d8d934a41bc6122e9951b1cefa9), [`b845b18`](https://github.com/Effect-TS/effect/commit/b845b186379106c17ed4b1149e2462cd0d2d77b5), [`381b794`](https://github.com/Effect-TS/effect/commit/381b79464f8e0a520585fdcfdd3e95e0553d0b27), [`4ffcaf4`](https://github.com/Effect-TS/effect/commit/4ffcaf4c4a94d3d9d5715db91aa37d7f5d9ecd99), [`ba2fd82`](https://github.com/Effect-TS/effect/commit/ba2fd822e9618f30d0f430ae4eed2eaa4813e2e3), [`02be94c`](https://github.com/Effect-TS/effect/commit/02be94ce78a3c9fc1e4c516d37d8e11e1c0eef2e), [`115d8c2`](https://github.com/Effect-TS/effect/commit/115d8c22599640ece2fd6a10564925b1d79f8a8c), [`1452635`](https://github.com/Effect-TS/effect/commit/14526354f3d0288bd1d0a85c8d4aeee8abe2cd9b), [`77f85fe`](https://github.com/Effect-TS/effect/commit/77f85fe1613348f5c990016b49dc97e252576c82), [`a3f2b31`](https://github.com/Effect-TS/effect/commit/a3f2b319343f28edb028270f7074066cea0ec429), [`310f8d3`](https://github.com/Effect-TS/effect/commit/310f8d3424a025ce1f1cc5c338232db2362052d8), [`291d616`](https://github.com/Effect-TS/effect/commit/291d616282b666bbba3664143fa1d19e839a6a5d), [`48dbbb2`](https://github.com/Effect-TS/effect/commit/48dbbb22bb2e04f63992fc267dd711a0cbd76d72), [`b43bfd6`](https://github.com/Effect-TS/effect/commit/b43bfd644a3205f1518abbfd006f761e31b3a5f9), [`1c89c78`](https://github.com/Effect-TS/effect/commit/1c89c78f9faa3638f398772f3ed7182aa97a1edd), [`9bbe1a5`](https://github.com/Effect-TS/effect/commit/9bbe1a5940eee4642ffe854356128b617e0a064c), [`dd99ab0`](https://github.com/Effect-TS/effect/commit/dd99ab007e3352761187dae330d52f65feeff7c0), [`8f397ed`](https://github.com/Effect-TS/effect/commit/8f397ed4e7852026591819cc4c8c1995bdd2fb8c), [`d7ae6b6`](https://github.com/Effect-TS/effect/commit/d7ae6b6491a88f2710612bfcddaf608ebe925f7c), [`8cf1203`](https://github.com/Effect-TS/effect/commit/8cf12030e8a3e1e76384884d41afa96e2bd72b13), [`87654c5`](https://github.com/Effect-TS/effect/commit/87654c5eed150387f4eac7153751b6085616ba52), [`f05ae0b`](https://github.com/Effect-TS/effect/commit/f05ae0b3fd2cfc7f12b668af244b3cf3fae59bc5), [`436f5eb`](https://github.com/Effect-TS/effect/commit/436f5eb44aa234bdf1de37b07da2ce8dc31ad4ec), [`d8ff960`](https://github.com/Effect-TS/effect/commit/d8ff9605bb5ef7b0cacd80e3b43ed1ce9e3d5e36), [`8766475`](https://github.com/Effect-TS/effect/commit/876647502300b9fb79d163de10fd497716d93be0), [`b64f406`](https://github.com/Effect-TS/effect/commit/b64f406de13a1b9da4664dbde2dbddaa5f776a05), [`4697aaa`](https://github.com/Effect-TS/effect/commit/4697aaa1d38f9da7fe0bcb62f708183f766fe496), [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494), [`9956f0e`](https://github.com/Effect-TS/effect/commit/9956f0e6f47096a6e31305ec2c7320b8e1a140af), [`5c7eed0`](https://github.com/Effect-TS/effect/commit/5c7eed0c5aa0988716206dd374359223edf828ee), [`183c2ea`](https://github.com/Effect-TS/effect/commit/183c2ea65ee1e9b967c746aa1b0bf2b8d3422de2), [`1e92dbd`](https://github.com/Effect-TS/effect/commit/1e92dbd26f5a74bb32103cbaac0f09460b7954de), [`4eb0fa7`](https://github.com/Effect-TS/effect/commit/4eb0fa72302b0a63ac34c6515a9cdf96338e586a), [`534b8b9`](https://github.com/Effect-TS/effect/commit/534b8b9dba195ec38a4795fe564d5e0876cb6468), [`b76a1cf`](https://github.com/Effect-TS/effect/commit/b76a1cf32b0a742c24c1874137e2da8fed5c48eb), [`acc1e53`](https://github.com/Effect-TS/effect/commit/acc1e5384aa5cb7b072f60308013f078b0099bdf), [`4950a91`](https://github.com/Effect-TS/effect/commit/4950a914a68306531a1111d4cdc4504aa8d3854e), [`c020987`](https://github.com/Effect-TS/effect/commit/c020987a0611c9e610a7eef347648b60026745b8), [`abe95d1`](https://github.com/Effect-TS/effect/commit/abe95d12340e1dd435e5df623b1967bf01938c94), [`027ceb9`](https://github.com/Effect-TS/effect/commit/027ceb91e003fbc36f0559f72e1c614cd9198c1a), [`3d203b7`](https://github.com/Effect-TS/effect/commit/3d203b702e26687d2fea4aaae7cc01dcfb82c05e), [`a29b8f4`](https://github.com/Effect-TS/effect/commit/a29b8f4de5aee58dd9ee645365c04cfe1ea6fa9f), [`ce4aa65`](https://github.com/Effect-TS/effect/commit/ce4aa65409e8500908fb6496eca06270ef5b636f), [`a8ea807`](https://github.com/Effect-TS/effect/commit/a8ea80799a0bf187c577e052fef0b7245494c7bf), [`a3ebb7f`](https://github.com/Effect-TS/effect/commit/a3ebb7fcb208f3933a40a0fd949dbcfdf1ca2921), [`473bd81`](https://github.com/Effect-TS/effect/commit/473bd814a97dba66070435cfe84933bcc143effe), [`53843f6`](https://github.com/Effect-TS/effect/commit/53843f6490f4eebaf1eeb91fdcc5f1c542b0e132), [`c5eca65`](https://github.com/Effect-TS/effect/commit/c5eca6539f0d605acd5ce19437fa88f167d69bb7), [`d6f9eba`](https://github.com/Effect-TS/effect/commit/d6f9eba7e8df0c8bd74762c313402884d94ba146), [`8d1e97a`](https://github.com/Effect-TS/effect/commit/8d1e97adbf5a36b4b53ab56f797c2ec0267a8821), [`b28ab48`](https://github.com/Effect-TS/effect/commit/b28ab48be7cf7f2bd30f9467ccac0ab41260f7ad), [`9960708`](https://github.com/Effect-TS/effect/commit/9960708052fcaaa873c471aea144ca46955e6023), [`8ac53b6`](https://github.com/Effect-TS/effect/commit/8ac53b6ae677f06d88d7d81f932ab5ff9b59a10e), [`84ad49a`](https://github.com/Effect-TS/effect/commit/84ad49a357ef156b4ff46cefc945efbe3e292eff), [`72cfa24`](https://github.com/Effect-TS/effect/commit/72cfa2420977919ba57a8e6de538e01e06d5f380), [`95c2581`](https://github.com/Effect-TS/effect/commit/95c2581e0f78e1537aad21764fe562206ca50adb), [`d5c7cd2`](https://github.com/Effect-TS/effect/commit/d5c7cd2c47b2b01523aaea48d2dd039b89b36114), [`fe4fed1`](https://github.com/Effect-TS/effect/commit/fe4fed1b014c78043a8efe37520c527100910010), [`d150a64`](https://github.com/Effect-TS/effect/commit/d150a643c4f60da2dc19ae813b807d48b580087e), [`05b1e80`](https://github.com/Effect-TS/effect/commit/05b1e80cde3a8da1ff1afe288f8539b72c05bb87), [`414dc90`](https://github.com/Effect-TS/effect/commit/414dc9056a39aa22f7cc27aeb5e631231ce50982), [`3f51acd`](https://github.com/Effect-TS/effect/commit/3f51acdf5e2490d7d0a833cd67ce06d1d867facd), [`d68ff05`](https://github.com/Effect-TS/effect/commit/d68ff05506ab5bb3ec07e1b759ce296423b0dbbe), [`f3cf1e6`](https://github.com/Effect-TS/effect/commit/f3cf1e6c36f1aac8399d9b540aecb6410be3096f), [`6525771`](https://github.com/Effect-TS/effect/commit/6525771eef7df6b71a3ef6e299ee93d798ed3dda), [`4ab4e83`](https://github.com/Effect-TS/effect/commit/4ab4e838a4c9ab3c9cc0d52d3e83de88686601b0), [`f7f1d78`](https://github.com/Effect-TS/effect/commit/f7f1d78e52023bdfbebf76949c550db8bb9411dd), [`47b358a`](https://github.com/Effect-TS/effect/commit/47b358a4b85d22973a7957abd1a859f65b75b7da), [`45ffa72`](https://github.com/Effect-TS/effect/commit/45ffa7221259c7e4be9797b90f6ca569fe7ec8a3), [`6232650`](https://github.com/Effect-TS/effect/commit/62326505a74fcd8fe27c8e2064128d2dea484ecc), [`4b73e1b`](https://github.com/Effect-TS/effect/commit/4b73e1bd5576346d7939be8915f3053e428d1c0e), [`284050c`](https://github.com/Effect-TS/effect/commit/284050c2aa6e78eceb08e5eff1803501cc80a658), [`c85fc0b`](https://github.com/Effect-TS/effect/commit/c85fc0b8f2195780929d6fc8ecdc084faf18736c), [`7999b07`](https://github.com/Effect-TS/effect/commit/7999b078950fda15e235db1e61e8db5eb0632528), [`7d455f5`](https://github.com/Effect-TS/effect/commit/7d455f51acf2f0e9f710f298e14e4d81179a9259), [`d681c2e`](https://github.com/Effect-TS/effect/commit/d681c2eb1bb07d782540286368e8b42533df3063), [`84d2a47`](https://github.com/Effect-TS/effect/commit/84d2a470ecc0fcad7f1d1e7a50464e02af0e6972), [`ed74b18`](https://github.com/Effect-TS/effect/commit/ed74b18b95b0797808f2f7928f413f1cf2279d77), [`2245997`](https://github.com/Effect-TS/effect/commit/2245997ad2214591b7d9674777595092926743cf), [`d386979`](https://github.com/Effect-TS/effect/commit/d386979fedbdd433d28816f9cc74675a9af45189), [`fc9fedf`](https://github.com/Effect-TS/effect/commit/fc9fedf59fc26a8bc6e45c3860a921f3b7e0f971), [`7750dbe`](https://github.com/Effect-TS/effect/commit/7750dbe0d5b9929c6d92b0365b42784b993ea2a6), [`fc91af6`](https://github.com/Effect-TS/effect/commit/fc91af659d0682659e6347eede288f90f618acc0), [`39b9738`](https://github.com/Effect-TS/effect/commit/39b9738727ab4d87298fd640edd138f352c70ee3), [`88093b5`](https://github.com/Effect-TS/effect/commit/88093b57951a1b597599fe4772ab690b36a95818), [`d14c463`](https://github.com/Effect-TS/effect/commit/d14c4631ac82911a8ca0b6c24c14ecb95863ee6b), [`d473bd3`](https://github.com/Effect-TS/effect/commit/d473bd3ee966a3a0c245d0e3c752fd91368b8ff6), [`84864bc`](https://github.com/Effect-TS/effect/commit/84864bc30c9e92a1226f65bb78b0641a7e0acea2), [`e2ae724`](https://github.com/Effect-TS/effect/commit/e2ae72481e755a454f28ce45e90f43b857b7c6e5), [`f921ed3`](https://github.com/Effect-TS/effect/commit/f921ed305c63415dc468e94d9319cc3237119138), [`1df933d`](https://github.com/Effect-TS/effect/commit/1df933d1dee2e09a25f9c27214b31aa94fa9d020), [`829aff9`](https://github.com/Effect-TS/effect/commit/829aff97d71d7900b7765ba2f63e33874b898cdc), [`aa0aba3`](https://github.com/Effect-TS/effect/commit/aa0aba383382ca1b99fbe618dceba63c3a5a6a53), [`a71140f`](https://github.com/Effect-TS/effect/commit/a71140fbe79809aa5311a55b1c6908905dc2df26), [`0276a27`](https://github.com/Effect-TS/effect/commit/0276a27ddbb15d163daf56f059ca9f62c4fd9939), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`c86c999`](https://github.com/Effect-TS/effect/commit/c86c99939e1628813a2f92f0906cfbbe924504e8), [`975f758`](https://github.com/Effect-TS/effect/commit/975f7586f3dcda5743d6570a740013a9fbdce9a7), [`2a3a478`](https://github.com/Effect-TS/effect/commit/2a3a4783bcd2d4e91e3505c7a6c1e398fea89595), [`1e6e206`](https://github.com/Effect-TS/effect/commit/1e6e206f6327a114f55a541775f465c0bc9fbf3e), [`fa6027b`](https://github.com/Effect-TS/effect/commit/fa6027b16e65376078e526fa33116ac0cc82485b), [`7616f73`](https://github.com/Effect-TS/effect/commit/7616f73c16bd45a3a3e2202d3f81c6d1cc63f4e0), [`fc668b6`](https://github.com/Effect-TS/effect/commit/fc668b61e9755545a47582dee6c4425d5652cbb6), [`d425c8c`](https://github.com/Effect-TS/effect/commit/d425c8c1319519e861714bcb9d055b63a9434fad), [`ce120f4`](https://github.com/Effect-TS/effect/commit/ce120f474b87d8a5a787dd7f848177ecd3b960fc), [`248201f`](https://github.com/Effect-TS/effect/commit/248201f7846d855685a4dc1b2f01968981706d69), [`e80d397`](https://github.com/Effect-TS/effect/commit/e80d3972fcca4e986b6497d67406f45ee1be540d), [`f1a941d`](https://github.com/Effect-TS/effect/commit/f1a941d52c2f71973e998883869e4e9ef510abf2), [`73bc3a1`](https://github.com/Effect-TS/effect/commit/73bc3a13b6e1b7a060e6bcd19ae7796443211a80), [`b628bb1`](https://github.com/Effect-TS/effect/commit/b628bb1240a09df2244c3ff6c587f978feb25225), [`6e3ae7b`](https://github.com/Effect-TS/effect/commit/6e3ae7b6359bda08cea74b761fd54224d7af45e2), [`e891247`](https://github.com/Effect-TS/effect/commit/e8912473bd4128714a0f8cb04621f3e12f62a910), [`53e6c73`](https://github.com/Effect-TS/effect/commit/53e6c7311b948a62e93e57f6daee66a8ec24b030), [`1579d6f`](https://github.com/Effect-TS/effect/commit/1579d6fd063e444e4fcdfd08ff194c7485203f9c), [`d3c6b73`](https://github.com/Effect-TS/effect/commit/d3c6b73cc4b78f22fdb2e25c8bebda027551540b), [`4a59c6a`](https://github.com/Effect-TS/effect/commit/4a59c6a559e69d399b00c6acd48c8e1977e34875), [`145d8e1`](https://github.com/Effect-TS/effect/commit/145d8e1013220425b8edf34f7011c73f73e1cdcf), [`f74282c`](https://github.com/Effect-TS/effect/commit/f74282cb08de808b678ce67687acbcd61939d2f6), [`0a38623`](https://github.com/Effect-TS/effect/commit/0a3862349ebfbc41746aa0de5e682992719c1da6), [`0a08ae0`](https://github.com/Effect-TS/effect/commit/0a08ae0626f8124779ec2e32ae2088d28db157c6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`d60c5d4`](https://github.com/Effect-TS/effect/commit/d60c5d40b5954ea1557aef1502b4b87f98bbf134), [`07ffd25`](https://github.com/Effect-TS/effect/commit/07ffd25bd0e82c9e6b72b8e669833fb4531a9da3), [`9b517ad`](https://github.com/Effect-TS/effect/commit/9b517ad28a0c2f213693bcab408658c40e3b6d9b), [`604b1c1`](https://github.com/Effect-TS/effect/commit/604b1c109d6c9e654b9b7fcc98d6b80361400325), [`ccc2e02`](https://github.com/Effect-TS/effect/commit/ccc2e02def5965f5535ca97fac2051f4387c4e13), [`14d810a`](https://github.com/Effect-TS/effect/commit/14d810a726caad8a7b0d42f04a6876f25fbcc005), [`a9d1ee3`](https://github.com/Effect-TS/effect/commit/a9d1ee3d4d51e97ea33440fe6100935d5fd5aada), [`a31adbe`](https://github.com/Effect-TS/effect/commit/a31adbefd6c04c70536de11433edba9edd1f5013), [`7245f87`](https://github.com/Effect-TS/effect/commit/7245f87ace09806d15893df8f0bac8f0b9b817db), [`3a0828b`](https://github.com/Effect-TS/effect/commit/3a0828b058abb4b4646bd880458b4e636d77d1c3), [`cd83544`](https://github.com/Effect-TS/effect/commit/cd83544dd594b99e8ea0d7a7645c086f5861769b), [`6550a07`](https://github.com/Effect-TS/effect/commit/6550a074c5c501d8a95dcd354f17e9785a1ae1eb), [`6f090d4`](https://github.com/Effect-TS/effect/commit/6f090d4c2032cc35f7f9a9eac900d0329b8ac07e), [`b505c0d`](https://github.com/Effect-TS/effect/commit/b505c0db03e86eb126237690a5867f457eb4237f), [`186dd49`](https://github.com/Effect-TS/effect/commit/186dd4914084fa346a870ce9c637b9c2a6cc8100), [`9f37e58`](https://github.com/Effect-TS/effect/commit/9f37e58d00e06150d96af48fffe780947ca0d519), [`4446451`](https://github.com/Effect-TS/effect/commit/44464510114773e1ebd1abb6efffa382cf69998e), [`58be972`](https://github.com/Effect-TS/effect/commit/58be97238ee1f6cd26f6a3a645d1df97a091a224), [`1320075`](https://github.com/Effect-TS/effect/commit/1320075fd4ed381f60f6f37f280dbec17cb29b81), [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80), [`f7490d4`](https://github.com/Effect-TS/effect/commit/f7490d49c75b99118c6c42549511ffa166459f59), [`c8ea602`](https://github.com/Effect-TS/effect/commit/c8ea60231ad69e23c0bf319f6f6b3ff28fc45e4b), [`62d82f4`](https://github.com/Effect-TS/effect/commit/62d82f4919c72d8cc0cfd2b704ed6900f69ad9a8), [`a2c9e7c`](https://github.com/Effect-TS/effect/commit/a2c9e7c17a4118b74a1eab3332042dafe0bb7318), [`97dd022`](https://github.com/Effect-TS/effect/commit/97dd022fe7fbd1696dd60f3319eae594abdd3760), [`f984ee8`](https://github.com/Effect-TS/effect/commit/f984ee82d255c7958eeed6f0dd08de1b88cbbfd6), [`f1b2910`](https://github.com/Effect-TS/effect/commit/f1b291027c3055f801c2f288dda21ba84c361956), [`a29e05a`](https://github.com/Effect-TS/effect/commit/a29e05a907053a8617763b4e7408e1f012b59049), [`1aa1d8b`](https://github.com/Effect-TS/effect/commit/1aa1d8bd0f2c2d429938657927f34f8ff0f940a0), [`bb99734`](https://github.com/Effect-TS/effect/commit/bb99734ef87bcc377c628bc4072aa856ce5f7ca6), [`222e7ca`](https://github.com/Effect-TS/effect/commit/222e7caa4bb0da6d81a4eabf25dffc181c20f6a3), [`797c9e3`](https://github.com/Effect-TS/effect/commit/797c9e3c1f2e66db1570cf6aa5c4a15e94507bf0), [`b4d5398`](https://github.com/Effect-TS/effect/commit/b4d5398598c04a84054a55873c943d587880058d), [`c8349ed`](https://github.com/Effect-TS/effect/commit/c8349ede1a2add807e9ec9c37612b3a39bf284f8), [`8426e5f`](https://github.com/Effect-TS/effect/commit/8426e5f7ad11f15e39565cf8ba0874bf974f5085), [`26e0085`](https://github.com/Effect-TS/effect/commit/26e0085d098f6e526c085c357b8d82ee068b6518), [`1a86166`](https://github.com/Effect-TS/effect/commit/1a86166f102b517ee4d89dca0095e28f738eee10), [`0856631`](https://github.com/Effect-TS/effect/commit/0856631a89d8f72d206c366905ddac75468a8e85), [`0af0985`](https://github.com/Effect-TS/effect/commit/0af0985d5c132b33e14a2df59e0567d7ba2a1710), [`bc582c9`](https://github.com/Effect-TS/effect/commit/bc582c94313cbb89d87b3844ffffabebbb4ffa83), [`2c63f1e`](https://github.com/Effect-TS/effect/commit/2c63f1e07e375b7958e14929c6a4b358c819a9d0), [`0d98213`](https://github.com/Effect-TS/effect/commit/0d9821346453d44696258103643d5482e63638b4), [`ca6f0dc`](https://github.com/Effect-TS/effect/commit/ca6f0dc4fcea564b351bf505df015d16a8b965ed), [`d8cc9ed`](https://github.com/Effect-TS/effect/commit/d8cc9ed80e1df9c056a46ba62df01f7fc0f531c3), [`42fd969`](https://github.com/Effect-TS/effect/commit/42fd969b45025eed6d60cb87b97c5ff88d187671), [`5641ad3`](https://github.com/Effect-TS/effect/commit/5641ad333a88bb9e56baf886006682678391be0a), [`629870d`](https://github.com/Effect-TS/effect/commit/629870d461b36d3dacb30c8d203b8d20d4beaba0), [`d592c14`](https://github.com/Effect-TS/effect/commit/d592c1492ad895ea72537e10387b735f3ba0e7de), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`53909a9`](https://github.com/Effect-TS/effect/commit/53909a9bf14b29725a08dcccde02bee8e95a6b14), [`78a4269`](https://github.com/Effect-TS/effect/commit/78a42697f6085c87c09b18e8095bb7541bd65261), [`06c6307`](https://github.com/Effect-TS/effect/commit/06c6307d3d5538bc398c67a44239bc3d52ab1a4d), [`0847c41`](https://github.com/Effect-TS/effect/commit/0847c41e2472a347cd8f77d460d3cbdfecd4adf0), [`5a77084`](https://github.com/Effect-TS/effect/commit/5a77084651bc58617b9988606473cf9ba5ca0032), [`96f99b3`](https://github.com/Effect-TS/effect/commit/96f99b325da32bfa535e826d89d838d0edec0dea), [`7bb8781`](https://github.com/Effect-TS/effect/commit/7bb878176b8ad20cf0e671b570caf72e194f9515), [`4907e9b`](https://github.com/Effect-TS/effect/commit/4907e9b2470894ddaf0cbdec2612b59f04a51dbe), [`2a30248`](https://github.com/Effect-TS/effect/commit/2a30248b6eb739f22403456209bc468f2f4ef26a), [`8364ddd`](https://github.com/Effect-TS/effect/commit/8364ddda233d35ee472cc2356312317a60a148a2), [`ad67d8c`](https://github.com/Effect-TS/effect/commit/ad67d8c5a1b195ff5468a9f2e963c59aa0b3166f), [`ec0c087`](https://github.com/Effect-TS/effect/commit/ec0c0875262fb81c7992f5023776b8fa894c49b6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`a2c1ce6`](https://github.com/Effect-TS/effect/commit/a2c1ce62f04214d98d848f4cfe33d677e1a87d39), [`91e9af0`](https://github.com/Effect-TS/effect/commit/91e9af0d5f138933d3a131ca1cfbc52d3bc55481), [`7bd3f34`](https://github.com/Effect-TS/effect/commit/7bd3f34385a06157b249a351e32ffdcbe1cf2c37), [`ef16581`](https://github.com/Effect-TS/effect/commit/ef165814eae0a199fdca5b3c2839bb4fcdfd5b5d), [`1693a87`](https://github.com/Effect-TS/effect/commit/1693a877324273f5a23fa7d7fc892f4ee12f20c2), [`df3fc47`](https://github.com/Effect-TS/effect/commit/df3fc47609eda7843dae2b557c8fe834299e26b4), [`e11be41`](https://github.com/Effect-TS/effect/commit/e11be41dfc73fa6b5643ab9af7df18d6907927d7), [`2e39e8b`](https://github.com/Effect-TS/effect/commit/2e39e8bafef7d3fee9f3b324b7d50cc064376667), [`11c5ee7`](https://github.com/Effect-TS/effect/commit/11c5ee7202acd3bb789d0a8ce78e6c4523071553), [`1742d2f`](https://github.com/Effect-TS/effect/commit/1742d2f48441cb7b1a8d9aca3a8e6f0acbc35a96), [`8efc70e`](https://github.com/Effect-TS/effect/commit/8efc70ea16bee43fd9adde2871cf82a8131b80a0), [`9642776`](https://github.com/Effect-TS/effect/commit/964277661423f398f777ec2eab4b3ecc0b53f53b), [`6680828`](https://github.com/Effect-TS/effect/commit/668082885f52672e4c7fad4e0fe2bfa0e69703e2), [`c34edcb`](https://github.com/Effect-TS/effect/commit/c34edcb6d7a6151d53e19828c92abf7dd8450c2a), [`7b2c5bd`](https://github.com/Effect-TS/effect/commit/7b2c5bd3c394fa4751fb229394ac9c03e401f441), [`1a2ccee`](https://github.com/Effect-TS/effect/commit/1a2ccee2bbb93514dc66e0a9cdeb52a82172ab0e), [`db995df`](https://github.com/Effect-TS/effect/commit/db995df19b0381b64cc39b50aa28d7bd8f2c1294), [`af0ccdd`](https://github.com/Effect-TS/effect/commit/af0ccdd13aa8917bce7161b8bc257fbe51c6a6a5), [`addeaea`](https://github.com/Effect-TS/effect/commit/addeaea0b0abe0dc24ca1d4d0fd194a0e458be70), [`7704034`](https://github.com/Effect-TS/effect/commit/770403411bcd8befef0552a0e40066abc417f6ea), [`e72b12f`](https://github.com/Effect-TS/effect/commit/e72b12fc305710550bc6dcb978e92de8abff88cd), [`310dd9c`](https://github.com/Effect-TS/effect/commit/310dd9ce9681e97f558bb042adb35f6040ab81e3), [`1c2afc1`](https://github.com/Effect-TS/effect/commit/1c2afc15bbf4ce5a91d24da99bc275b5bd744e77), [`44f44ca`](https://github.com/Effect-TS/effect/commit/44f44cae65249801c082c3a3848016eeeac5eb24), [`f43b9d6`](https://github.com/Effect-TS/effect/commit/f43b9d6d99cb591c0377bb1474121827201627c9), [`56e72b3`](https://github.com/Effect-TS/effect/commit/56e72b38c01bbe6ec921ad151f83b7849444f219), [`50ef80e`](https://github.com/Effect-TS/effect/commit/50ef80e251b85b5ee63aed06fe0244b5433ec2fa), [`fc3b718`](https://github.com/Effect-TS/effect/commit/fc3b7187114a31cbb21a250f55fda601bb3edcf0), [`ee336d8`](https://github.com/Effect-TS/effect/commit/ee336d80b4024b51654e7a2b8d869d244153fcdf), [`d12f922`](https://github.com/Effect-TS/effect/commit/d12f922853eff2310dbe9fc9b31562ad81b38435), [`46d8310`](https://github.com/Effect-TS/effect/commit/46d83101e8408c0428d9a8760e06e23a69ad6070), [`59812fd`](https://github.com/Effect-TS/effect/commit/59812fd221cde16fc77c03d326c6a2cf8fbec6b9), [`f9d0dec`](https://github.com/Effect-TS/effect/commit/f9d0dec12c5f229f05d7a74d532a85c6bbffefbf), [`4372c79`](https://github.com/Effect-TS/effect/commit/4372c79a32fcecc7b6b284d49a215b2c4fef9d77), [`81485ef`](https://github.com/Effect-TS/effect/commit/81485ef0288a3321e75d7d4ae32b7e0e06b6f86b), [`bd393d6`](https://github.com/Effect-TS/effect/commit/bd393d63c19bdd0ab212d95576cec89051c8501c), [`0c95c04`](https://github.com/Effect-TS/effect/commit/0c95c04b41c2d5d09e4564fd997572207fd72595)]: + - effect@4.0.0-rc.113 + ## 4.0.0-rc.112 ### Patch Changes diff --git a/repos/effect/packages/ai/openrouter/package.json b/repos/effect/packages/ai/openrouter/package.json index deda898f1..343a59435 100644 --- a/repos/effect/packages/ai/openrouter/package.json +++ b/repos/effect/packages/ai/openrouter/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openrouter", - "version": "4.0.0-rc.112", + "version": "4.0.0-rc.117", "type": "module", "license": "MIT", "description": "An OpenRouter provider integration for Effect AI SDK", diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts index 8a2e66429..5f57c9202 100644 --- a/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts @@ -1,8 +1,6 @@ /** - * The `OpenRouterClient` module provides an Effect service for calling - * OpenRouter's chat completions API. It wraps the generated OpenRouter HTTP - * client with Effect-native constructors, layers, authentication and optional - * site ranking headers, typed errors, and streaming support. + * HTTP client for OpenRouter's chat completions and alpha Decisions APIs, + * with authentication, site ranking headers, typed errors, and chat streaming. * * @since 4.0.0 */ @@ -15,15 +13,16 @@ import * as Predicate from "effect/Predicate" import type * as Redacted from "effect/Redacted" import * as Schema from "effect/Schema" import * as Stream from "effect/Stream" -import type * as AiError from "effect/unstable/ai/AiError" +import * as AiError from "effect/unstable/ai/AiError" import * as Sse from "effect/unstable/encoding/Sse" 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 type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" import * as Generated from "./Generated.ts" import * as Errors from "./internal/errors.ts" import { OpenRouterConfig } from "./OpenRouterConfig.ts" +import * as OpenRouterSchema from "./OpenRouterSchema.ts" // ============================================================================= // Service Interface @@ -43,6 +42,13 @@ import { OpenRouterConfig } from "./OpenRouterConfig.ts" export interface Service { readonly client: Generated.OpenRouterClient + readonly createDecisions: ( + options: typeof OpenRouterSchema.DecisionsRequest.Encoded + ) => Effect.Effect< + [body: typeof OpenRouterSchema.DecisionsResponse.Type, response: HttpClientResponse.HttpClientResponse], + AiError.AiError + > + readonly createChatCompletion: ( options: typeof Generated.ChatRequest.Encoded ) => Effect.Effect< @@ -111,6 +117,10 @@ export class OpenRouterClient extends Context.Service< export type Options = { readonly apiKey?: Redacted.Redacted | undefined + /** + * Base URL for the versioned API. Decisions replace a trailing "/v1" with + * "/alpha/decisions"; otherwise they append "/alpha/decisions" to this URL. + */ readonly apiUrl?: string | undefined /** @@ -140,10 +150,6 @@ export type Options = { /** * Creates an OpenRouter client service from explicit options. * - * **When to use** - * - * Use when you need the OpenRouter client service value inside an effect. - * * **Details** * * The returned service uses the current `HttpClient`, prepends `apiUrl` or @@ -151,11 +157,8 @@ export type Options = { * `HTTP-Referer` and `X-Title` headers, accepts JSON responses, and applies * `transformClient` when provided. * - * **Gotchas** - * * Scoped `OpenRouterConfig.withClientTransform` applies to generated client - * request methods. Streaming chat completion requests are sent directly by this - * module and do not read that scoped transform. + * methods and alpha Decisions requests, but not streaming chat completions. * * @see {@link layer} for providing this client from explicit options * @see {@link layerConfig} for loading client settings from `Config` @@ -167,21 +170,55 @@ export const make = Effect.fnUntraced( function*(options: Options): Effect.fn.Return { const baseClient = yield* HttpClient.HttpClient - const httpClient = baseClient.pipe( - HttpClient.mapRequest((request) => - request.pipe( - HttpClientRequest.prependUrl(options.apiUrl ?? "https://openrouter.ai/api/v1"), - options.apiKey ? HttpClientRequest.bearerToken(options.apiKey) : identity, - options.siteReferrer ? HttpClientRequest.setHeader("HTTP-Referer", options.siteReferrer) : identity, - options.siteTitle ? HttpClientRequest.setHeader("X-Title", options.siteTitle) : identity, - HttpClientRequest.acceptJson - ) - ), - options.transformClient ?? identity - ) + const makeHttpClient = (baseUrl: string) => + baseClient.pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(baseUrl), + options.apiKey ? HttpClientRequest.bearerToken(options.apiKey) : identity, + options.siteReferrer ? HttpClientRequest.setHeader("HTTP-Referer", options.siteReferrer) : identity, + options.siteTitle ? HttpClientRequest.setHeader("X-Title", options.siteTitle) : identity, + HttpClientRequest.acceptJson + ) + ), + options.transformClient ?? identity + ) + const httpClient = makeHttpClient(options.apiUrl ?? "https://openrouter.ai/api/v1") const httpClientOk = HttpClient.filterStatusOk(httpClient) + // Remove the version suffix for alpha requests, preserving proxy prefixes. + const decisionsClient = makeHttpClient( + (options.apiUrl ?? "https://openrouter.ai/api/v1").replace(/\/v1\/?$/, "") + ) + const createDecisions: Service["createDecisions"] = Effect.fnUntraced( + function*(payload) { + const config = yield* OpenRouterConfig.getOrUndefined + const client = HttpClient.filterStatusOk( + config?.transformClient ? config.transformClient(decisionsClient) : decisionsClient + ) + const request = yield* HttpClientRequest.bodyJson(HttpClientRequest.post("/alpha/decisions"), payload).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenRouterClient", + method: "createDecisions", + reason: new AiError.InvalidRequestError({ description: String(error) }) + }) + ) + ) + const response = yield* client.execute(request) + const body = yield* HttpClientResponse.schemaBodyJson(OpenRouterSchema.DecisionsResponse)(response) + return [body, response] as [ + typeof OpenRouterSchema.DecisionsResponse.Type, + HttpClientResponse.HttpClientResponse + ] + }, + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createDecisions"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createDecisions")) + }) + ) + const client = Generated.make(httpClient, { transformClient: Effect.fnUntraced(function*(client) { const config = yield* OpenRouterConfig.getOrUndefined @@ -255,6 +292,7 @@ export const make = Effect.fnUntraced( return OpenRouterClient.of({ client, + createDecisions, createChatCompletion, createChatCompletionStream }) diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts index e58445718..38a694af2 100644 --- a/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts @@ -1,8 +1,5 @@ /** - * The `OpenRouterConfig` module lets a workflow temporarily customize the HTTP - * client used by generated OpenRouter request methods. `OpenRouterClient` reads - * this scoped transform when generated client operations execute, so callers can - * add middleware or instrumentation without rebuilding the client layer. + * Scoped HTTP client customization for OpenRouter requests. * * @since 4.0.0 */ @@ -12,13 +9,7 @@ import { dual } from "effect/Function" import type { HttpClient } from "effect/unstable/http/HttpClient" /** - * Context service for scoped OpenRouter provider configuration used by client - * operations. - * - * **When to use** - * - * Use as the context service tag when manually providing or reading scoped - * OpenRouter provider configuration in an Effect context. + * Scoped configuration read when executing OpenRouter requests. * * @see {@link withClientTransform} for scoping an HTTP client transformation * @@ -47,8 +38,7 @@ export class OpenRouterConfig extends Context.Service< */ export declare namespace OpenRouterConfig { /** - * Configuration values read by OpenRouter provider operations when resolving - * the generated HTTP client. + * HTTP client configuration for generated methods and alpha Decisions requests. * * @category services * @since 4.0.0 @@ -59,26 +49,9 @@ export declare namespace OpenRouterConfig { } /** - * Provides a scoped transform for the OpenRouter HTTP client used by provider - * operations. - * - * **When to use** - * - * Use when you need temporary OpenRouter HTTP client customization for a - * single effect or workflow without rebuilding the client layer. - * - * **Details** - * - * Supports both data-first and data-last forms. The transform is stored in the - * scoped `OpenRouterConfig` service and read by generated OpenRouter request - * operations while running the supplied effect. - * - * **Gotchas** - * - * If a transform is already present in the scoped config, this helper replaces - * it. Compose transforms manually when both should apply. Streaming chat - * completion requests are sent directly by `OpenRouterClient.make` and do not - * read this scoped transform. + * Transforms the HTTP client for generated methods and alpha Decisions requests + * made by the supplied effect. Streaming chat completions ignore this transform. + * Replaces any existing scoped transform; compose them manually to apply both. * * @category configuration * @since 4.0.0 diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterDecisionModel.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterDecisionModel.ts new file mode 100644 index 000000000..4904ab18a --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterDecisionModel.ts @@ -0,0 +1,136 @@ +/** + * Decision models backed by OpenRouter's alpha Decisions API. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as AiError from "effect/unstable/ai/AiError" +import * as DecisionModel from "effect/unstable/ai/DecisionModel" +import * as Model from "effect/unstable/ai/Model" +import { OpenRouterClient } from "./OpenRouterClient.ts" +import type * as OpenRouterSchema from "./OpenRouterSchema.ts" + +/** + * Request options that override model defaults at call time. + * + * @category services + * @since 4.0.0 + */ +export class Config extends Context.Service< + Config, + Pick< + typeof OpenRouterSchema.DecisionsRequest.Encoded, + "provider" | "session_id" | "user" | "trace" + > +>()("@effect/ai-openrouter/OpenRouterDecisionModel/Config") {} + +/** + * Creates an OpenRouter model descriptor with decision support. + * + * @category constructors + * @since 4.0.0 + */ +export const model = ( + model: string, + config?: typeof Config.Service +): Model.Model<"openrouter", DecisionModel.DecisionModel, OpenRouterClient> => + Model.make("openrouter", model, layer({ model, config })) + +/** + * Creates a decision service that requires full choice and score distributions. + * Score indices map to criteria labels; cost, id, and provider metadata are omitted. + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced(function*(options: { + readonly model: string + readonly config?: typeof Config.Service | undefined +}): Effect.fn.Return { + const client = yield* OpenRouterClient + const config = Effect.contextWith((services: Context.Context) => + Effect.succeed({ ...options.config, ...Context.getOrUndefined(services, Config) }) + ) + return yield* DecisionModel.make({ + decide: Effect.fnUntraced(function*({ state, decisions }) { + if (state === null || typeof state === "number" || typeof state === "boolean") { + return yield* AiError.make({ + module: "OpenRouterDecisionModel", + method: "decide", + reason: new AiError.InvalidUserInputError({ + description: "OpenRouter decision state must be a string, object, or array" + }) + }) + } + const questions: Record = Object.create(null) + for (const [key, decision] of Object.entries(decisions)) { + switch (decision._tag) { + case "Classify": + questions[key] = { type: "choice", instructions: decision.instructions, criteria: decision.criteria } + break + case "Rate": + questions[key] = { type: "score", instructions: decision.instructions, criteria: decision.criteria } + break + case "Probability": + questions[key] = { type: "noul", instructions: decision.instructions, criteria: decision.criteria } + break + } + } + const [response] = yield* client.createDecisions({ ...yield* config, model: options.model, state, questions }) + const answers: Record = Object.create(null) + for (const [key, answer] of Object.entries(response.answers)) { + if (answer.type === "noul") { + answers[key] = { _tag: "Probability", probability: answer.noul } + continue + } + if (answer.probabilities === undefined) { + return yield* AiError.make({ + module: "OpenRouterDecisionModel", + method: "decide", + reason: new AiError.InvalidOutputError({ + description: `Provider returned no probabilities for decision "${key}"` + }) + }) + } + if (answer.type === "choice") { + answers[key] = { + _tag: "Classify", + label: answer.choice, + probabilities: answer.probabilities, + confidence: answer.confidence + } + } else { + const decision = decisions[key] + const probabilities: Record = Object.create(null) + if (decision?._tag === "Rate") { + for (const [index, label] of decision.criteria.entries()) { + const probability = answer.probabilities[String(index)] + if (probability !== undefined) { + probabilities[label] = probability + } + } + } + answers[key] = { _tag: "Rate", rating: answer.score, probabilities, confidence: answer.confidence } + } + } + return { + answers, + usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens } + } + }) + }) +}) + +/** + * Provides a decision model using the OpenRouter client service. + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: { + readonly model: string + readonly config?: typeof Config.Service | undefined +}): Layer.Layer => + Layer.effect(DecisionModel.DecisionModel, make(options)) diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts index b32664cd9..0a8d76918 100644 --- a/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts @@ -516,7 +516,7 @@ export const model = ( * * **When to use** * - * Use when you need to construct a `LanguageModel.Service` value backed by + * Use when you need to construct a `LanguageModel` value backed by * `OpenRouterClient` inside an Effect. * * **Details** @@ -541,7 +541,7 @@ export const model = ( export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { readonly model: string readonly config?: Omit | undefined -}): Effect.fn.Return { +}): Effect.fn.Return { const client = yield* OpenRouterClient const codecTransformer = getCodecTransformer(model) @@ -557,8 +557,9 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig const messages = yield* prepareMessages({ options }) const { tools, toolChoice } = yield* prepareTools({ options, transformer: codecTransformer }) const responseFormat = yield* getResponseFormat({ config, options, transformer: codecTransformer }) + const { strictJsonSchema: _sjs, ...apiConfig } = config const request: typeof Generated.ChatRequest.Encoded = { - ...config, + ...apiConfig, messages, ...(Predicate.isNotUndefined(responseFormat) ? { response_format: responseFormat } : undefined), ...(Predicate.isNotUndefined(tools) ? { tools } : undefined), @@ -899,7 +900,7 @@ const prepareMessages = Effect.fnUntraced( messages.push({ role: "tool", tool_call_id: part.id, - content: JSON.stringify(part.result) + content: typeof part.result === "string" ? part.result : JSON.stringify(part.result) }) } @@ -1046,7 +1047,6 @@ const makeResponse = Effect.fnUntraced( method: "makeResponse", reason: new AiError.ToolParameterValidationError({ toolName, - toolParams: {}, description: `Failed to securely JSON parse tool parameters: ${cause}` }) }) @@ -1495,7 +1495,7 @@ const makeStreamResponse = Effect.fnUntraced( (detail) => detail.type === "reasoning.encrypted" && detail.data.length > 0 ) if (totalToolCalls > 0 && hasEncryptedReasoning && finishReason === "stop") { - finishReason = resolveFinishReason("tool-calls") + finishReason = "tool-calls" } // Forward any unsent tool calls if finish reason is 'tool-calls' @@ -1844,16 +1844,21 @@ const getUsage = (usage: Generated.ChatUsage | undefined): Response.Usage => { const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0 const cacheWriteTokens = usage.prompt_tokens_details?.cache_write_tokens ?? 0 const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0 + // Some providers report cached or reasoning tokens separately from their parent counts. + // Treat details exceeding the parent as disjoint to avoid negative remainders. + // Otherwise, retain subset accounting. + const inputTotal = cacheReadTokens > promptTokens ? promptTokens + cacheReadTokens : promptTokens + const outputTotal = reasoningTokens > completionTokens ? completionTokens + reasoningTokens : completionTokens return { inputTokens: { - uncached: promptTokens - cacheReadTokens, - total: promptTokens, + uncached: inputTotal - cacheReadTokens, + total: inputTotal, cacheRead: cacheReadTokens, cacheWrite: cacheWriteTokens }, outputTokens: { - total: completionTokens, - text: completionTokens - reasoningTokens, + total: outputTotal, + text: outputTotal - reasoningTokens, reasoning: reasoningTokens } } diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterSchema.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterSchema.ts new file mode 100644 index 000000000..76cc89521 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterSchema.ts @@ -0,0 +1,112 @@ +/** + * Schemas for OpenRouter's alpha Decisions API. + * + * @since 4.0.0 + */ +import * as Schema from "effect/Schema" +import * as Generated from "./Generated.ts" + +/** + * A choice question with descriptions keyed by choice label. + * + * @category schemas + * @since 4.0.0 + */ +export const ChoiceQuestion = Schema.Struct({ + type: Schema.Literal("choice"), + instructions: Schema.String, + criteria: Schema.Record(Schema.String, Schema.String) +}) + +/** + * A score question with ordered criteria. + * + * @category schemas + * @since 4.0.0 + */ +export const ScoreQuestion = Schema.Struct({ + type: Schema.Literal("score"), + instructions: Schema.String, + criteria: Schema.Array(Schema.String) +}) + +/** + * A probability question with optional true and false descriptions. + * + * @category schemas + * @since 4.0.0 + */ +export const NoulQuestion = Schema.Struct({ + type: Schema.Literal("noul"), + instructions: Schema.String, + criteria: Schema.optional(Schema.Struct({ true: Schema.String, false: Schema.String })) +}) + +/** + * A choice, ordered score, or probability question. + * + * @category schemas + * @since 4.0.0 + */ +export const DecisionsQuestion = Schema.Union([ + ChoiceQuestion, + ScoreQuestion, + NoulQuestion +]) + +/** + * A provider answer with optional probabilities and confidence. + * + * @category schemas + * @since 4.0.0 + */ +export const Answer = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("choice"), + choice: Schema.String, + confidence: Schema.optional(Schema.Finite), + probabilities: Schema.optional(Schema.Record(Schema.String, Schema.Finite)) + }), + Schema.Struct({ + type: Schema.Literal("score"), + score: Schema.Finite, + confidence: Schema.optional(Schema.Finite), + probabilities: Schema.optional(Schema.Record(Schema.String, Schema.Finite)), + legend: Schema.optional(Schema.Record(Schema.String, Schema.String)) + }), + Schema.Struct({ type: Schema.Literal("noul"), noul: Schema.Finite }) +]) + +/** + * Request body for the alpha Decisions endpoint. + * + * @category schemas + * @since 4.0.0 + */ +export const DecisionsRequest = Schema.Struct({ + model: Schema.String, + state: Schema.Unknown, + questions: Schema.Record(Schema.String, DecisionsQuestion), + provider: Schema.optional(Generated.ProviderPreferences), + session_id: Schema.optional(Schema.String), + user: Schema.optional(Schema.String), + trace: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)) +}) + +/** + * Response body for the alpha Decisions endpoint. + * + * @category schemas + * @since 4.0.0 + */ +export const DecisionsResponse = Schema.Struct({ + model: Schema.String, + answers: Schema.Record(Schema.String, Answer), + usage: Schema.Struct({ + input_tokens: Schema.Int, + output_tokens: Schema.Int, + cost: Schema.optional(Schema.Finite) + }), + id: Schema.optional(Schema.String), + provider: Schema.optional(Schema.String) +}) diff --git a/repos/effect/packages/ai/openrouter/src/index.ts b/repos/effect/packages/ai/openrouter/src/index.ts index 2ea005eb1..7f5d1ab1d 100644 --- a/repos/effect/packages/ai/openrouter/src/index.ts +++ b/repos/effect/packages/ai/openrouter/src/index.ts @@ -5,7 +5,7 @@ // @barrel: Auto-generated exports. Do not edit manually. /** - * @since 4.0.0 + * @since 1.0.0 */ export * as Generated from "./Generated.ts" @@ -19,6 +19,11 @@ export * as OpenRouterClient from "./OpenRouterClient.ts" */ export * as OpenRouterConfig from "./OpenRouterConfig.ts" +/** + * @since 4.0.0 + */ +export * as OpenRouterDecisionModel from "./OpenRouterDecisionModel.ts" + /** * @since 4.0.0 */ @@ -28,3 +33,8 @@ export * as OpenRouterError from "./OpenRouterError.ts" * @since 4.0.0 */ export * as OpenRouterLanguageModel from "./OpenRouterLanguageModel.ts" + +/** + * @since 4.0.0 + */ +export * as OpenRouterSchema from "./OpenRouterSchema.ts" diff --git a/repos/effect/packages/ai/openrouter/test/Generated.test.ts b/repos/effect/packages/ai/openrouter/test/Generated.test.ts index 8790fed73..b81d3745d 100644 --- a/repos/effect/packages/ai/openrouter/test/Generated.test.ts +++ b/repos/effect/packages/ai/openrouter/test/Generated.test.ts @@ -1,7 +1,9 @@ -import { Generated } from "@effect/ai-openrouter" +import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" import { describe, it } from "@effect/vitest" import { deepStrictEqual } from "@effect/vitest/utils" -import { Schema } from "effect" +import { Effect, Schema, Stream } from "effect" +import { type AiError, LanguageModel, type Response, Tool, Toolkit } from "effect/unstable/ai" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("Generated", () => { it("decodes nullable generation statistics", () => { @@ -72,4 +74,86 @@ describe("Generated", () => { deepStrictEqual(Schema.decodeUnknownSync(Generated.ChatUsage)(usage), usage) }) + + for ( + const { encrypted, mode, reason, tool } of [ + { mode: "stream", encrypted: true, tool: true, reason: "tool-calls" }, + { mode: "stream", encrypted: false, tool: true, reason: "stop" }, + { mode: "stream", encrypted: true, tool: false, reason: "stop" }, + { mode: "generate", encrypted: true, tool: true, reason: "tool-calls" } + ] as const + ) { + it.effect(`${mode}: encrypted=${encrypted}, tool=${tool} finishes with ${reason}`, () => + Effect.gen(function*() { + const metadata = { id: "response-1", model: "test/reasoning-model", created: 1 } + const reasoningDetails = encrypted + ? [{ type: "reasoning.encrypted", data: "opaque-signature", format: "unknown" } as const] + : [] + const toolCalls = tool + ? [{ + index: 0, + id: "call-1", + type: "function" as const, + function: { name: "ProbeTool", arguments: "{\"value\":1}" } + }] + : [] + const usage = { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } + const chunks = yield* Schema.decodeUnknownEffect(Schema.Array(Generated.ChatStreamChunk))([ + { + ...metadata, + object: "chat.completion.chunk", + choices: [{ index: 0, delta: { reasoning_details: reasoningDetails, tool_calls: toolCalls } }] + }, + { + ...metadata, + object: "chat.completion.chunk", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage + } + ]) + const body = yield* Schema.decodeUnknownEffect(Generated.SendChatCompletionRequest200)({ + ...metadata, + object: "chat.completion", + system_fingerprint: null, + choices: [{ + index: 0, + finish_reason: "stop", + message: { role: "assistant", reasoning_details: reasoningDetails, tool_calls: toolCalls } + }], + usage + }) + const response = HttpClientResponse.fromWeb( + HttpClientRequest.post("https://example.com/chat/completions"), + new globalThis.Response() + ) + const toolkit = Toolkit.make(Tool.make("ProbeTool", { + parameters: Schema.Struct({ value: Schema.Number }), + success: Schema.String + })) + const client = OpenRouterClient.OpenRouterClient.of({ + client: Generated.make(HttpClient.make(() => Effect.die("Unexpected HTTP request"))), + createDecisions: () => Effect.die("Unexpected decisions request"), + createChatCompletion: () => Effect.succeed([body, response]), + createChatCompletionStream: () => Effect.succeed([response, Stream.fromIterable(chunks)]) + }) + const options = { prompt: "Use the probe tool", toolkit, disableToolCallResolution: true } as const + const operation: Effect.Effect, AiError.AiError, LanguageModel.LanguageModel> = + mode === "stream" + ? LanguageModel.streamText(options).pipe(Stream.runCollect) + : LanguageModel.generateText(options).pipe(Effect.map((result) => result.content)) + const parts = globalThis.Array.from( + yield* operation.pipe( + Effect.provide(OpenRouterLanguageModel.model(metadata.model)), + Effect.provide(toolkit.toLayer({ ProbeTool: () => Effect.succeed("ok") })), + Effect.provideService(OpenRouterClient.OpenRouterClient, client) + ) + ) + + deepStrictEqual( + parts.filter((part) => part.type === "tool-call").map((part) => part.name), + tool ? ["ProbeTool"] : [] + ) + deepStrictEqual(parts.filter((part) => part.type === "finish").map((part) => part.reason), [reason]) + })) + } }) diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts b/repos/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts index 3e5eac5a4..c71fb0fa8 100644 --- a/repos/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts +++ b/repos/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts @@ -1,10 +1,41 @@ import { OpenRouterClient } from "@effect/ai-openrouter" import * as Errors from "@effect/ai-openrouter/internal/errors" import { assert, describe, it } from "@effect/vitest" -import { Context, Effect, Layer, Redacted, type Schema } from "effect" +import { Cause, Context, Effect, Exit, Layer, Option, Redacted, type Schema } from "effect" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("OpenRouterClient", () => { + it.effect("rejects unserializable bigint decision state with a typed error before making an HTTP call", () => + Effect.gen(function*() { + const client = yield* OpenRouterClient.OpenRouterClient + const exit = yield* client.createDecisions({ + model: "test-model", + state: { n: 1n }, + questions: { + urgent: { + type: "noul", + instructions: "Is this urgent?", + criteria: { false: "Not urgent", true: "Urgent" } + } + } + }).pipe(Effect.exit) + + assert.deepStrictEqual(yield* MockHttpClient.requests, []) + assert.isTrue(Exit.isFailure(exit)) + if (!Exit.isFailure(exit)) { + return yield* Effect.die(new Error("Expected serialization to fail")) + } + assert.isFalse(Cause.hasDies(exit.cause)) + const error = Option.getOrThrow(Cause.findErrorOption(exit.cause)) + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.module, "OpenRouterClient") + assert.strictEqual(error.method, "createDecisions") + assert.strictEqual(error.reason._tag, "InvalidRequestError") + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Json", + body: {} + })))) + it.effect("redacts the API key in AI error context", () => Effect.gen(function*() { const client = yield* OpenRouterClient.OpenRouterClient diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts b/repos/effect/packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts new file mode 100644 index 000000000..b46ef9d10 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts @@ -0,0 +1,767 @@ +import { OpenRouterClient, OpenRouterConfig, OpenRouterDecisionModel } from "@effect/ai-openrouter" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted, Schema } from "effect" +import { Decision, DecisionModel, Model } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +const Ticket = Schema.Struct({ + message: Schema.String, + orderId: Schema.String +}) + +const TicketTriage = Decision.make({ + input: Ticket, + decisions: { + department: Decision.classify({ + instructions: "Which team should handle this", + criteria: { + billing: "Payment or subscription issues", + technical: "Bugs or integration problems", + sales: "Pricing or account questions" + } + }), + frustration: Decision.rate({ + instructions: "How frustrated the customer appears", + criteria: ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"] + }), + urgent: Decision.probability({ + instructions: "The message conveys urgency or time-sensitivity", + criteria: { + false: "No time pressure", + true: "Needs action now" + } + }) + } +}) + +const ticket = { + message: "Hi, I've been trying to connect my Stripe account for 3 days and it keeps failing. Please help ASAP.", + orderId: "A-104" +} + +const decisionsResponse = { + model: "test/decision-model", + answers: { + department: { + type: "choice", + choice: "billing", + probabilities: { billing: 0.84, technical: 0.159, sales: 0.001 }, + confidence: 0.596 + }, + frustration: { + type: "score", + score: 1.6, + legend: { + "0": "Calm, just stating facts", + "1": "Frustrated but civil", + "2": "Very angry, strong language" + }, + probabilities: { "0": 0.05, "1": 0.3, "2": 0.65 }, + confidence: 0.78 + }, + urgent: { + type: "noul", + noul: 0.999 + } + }, + usage: { input_tokens: 312, output_tokens: 48 } +} + +describe("OpenRouterDecisionModel", () => { + it.effect("omits absent probability criteria from the encoded question", () => + Effect.gen(function*() { + const definition = Decision.make({ + input: Schema.String, + decisions: { + urgent: Decision.probability({ instructions: "Needs action now" }) + } + }) + const { answers } = yield* DecisionModel.decide(definition, { input: "Help ASAP" }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.gen(function*() { + const { questions } = yield* getRequestBody(request) + assert.deepStrictEqual(questions.urgent, { + type: "noul", + instructions: "Needs action now" + }) + + return jsonResponse(request, { + model: "test/decision-model", + answers: { urgent: { type: "noul", noul: 0.75 } }, + usage: { input_tokens: 20, output_tokens: 4 } + }) + }) + )) + ) + + assert.deepStrictEqual(answers.urgent, { probability: 0.75 }) + })) + + for (const key of ["__proto__", "constructor", "toString"]) { + it.effect("round trips a classification label named " + key, () => + Effect.gen(function*() { + const definition = Decision.make({ + input: Schema.String, + decisions: { + category: Decision.classify({ + instructions: "Choose a category", + criteria: { [key]: "Special category", ordinary: "Ordinary category" } + }) + } + }) + const { answers } = yield* DecisionModel.decide(definition, { input: "Help" }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.gen(function*() { + const { questions } = yield* getRequestBody(request) + const criteria = questions.category.criteria + + assert.isTrue(Object.hasOwn(criteria, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(criteria, key)?.enumerable) + assert.deepStrictEqual(Object.keys(criteria), [key, "ordinary"]) + assert.strictEqual(criteria[key], "Special category") + assert.strictEqual(Object.getPrototypeOf(criteria), Object.prototype) + + return jsonResponse(request, { + model: "test/decision-model", + answers: { + category: { + type: "choice", + choice: key, + probabilities: { [key]: 0.75, ordinary: 0.25 }, + confidence: 0.5 + } + }, + usage: { input_tokens: 10, output_tokens: 5 } + }) + }) + )) + ) + const { label, probabilities } = answers.category + + assert.isTrue(Object.hasOwn(probabilities, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(probabilities, key)?.enumerable) + assert.deepStrictEqual(Object.keys(probabilities), [key, "ordinary"]) + assert.deepStrictEqual(probabilities, { [key]: 0.75, ordinary: 0.25 }) + assert.strictEqual(label, key) + assert.strictEqual(Object.getPrototypeOf(probabilities), null) + })) + + it.effect("maps a score distribution to a rate level named " + key, () => + Effect.gen(function*() { + const definition = Decision.make({ + input: Schema.String, + decisions: { + intensity: Decision.rate({ instructions: "How intense", criteria: ["low", key, "high"] }) + } + }) + const { answers } = yield* DecisionModel.decide(definition, { input: "Help" }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.gen(function*() { + const { questions } = yield* getRequestBody(request) + assert.deepStrictEqual(questions.intensity.criteria, ["low", key, "high"]) + + return jsonResponse(request, { + model: "test/decision-model", + answers: { + intensity: { + type: "score", + score: 1.1, + legend: { "0": "low", "1": key, "2": "high" }, + probabilities: { "0": 0.1, "1": 0.7, "2": 0.2 }, + confidence: 0.5 + } + }, + usage: { input_tokens: 10, output_tokens: 5 } + }) + }) + )) + ) + const { label, probabilities, rating } = answers.intensity + + assert.isTrue(Object.hasOwn(probabilities, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(probabilities, key)?.enumerable) + assert.deepStrictEqual(Object.keys(probabilities), ["low", key, "high"]) + assert.deepStrictEqual(probabilities, { low: 0.1, [key]: 0.7, high: 0.2 }) + assert.strictEqual(label, key) + assert.strictEqual(rating, 1.1) + assert.strictEqual(Object.getPrototypeOf(probabilities), null) + })) + + it.effect("sends an own enumerable question named " + key, () => + Effect.gen(function*() { + const definition = Decision.make({ + input: Schema.String, + decisions: { + [key]: Decision.probability({ + instructions: "Needs action now", + criteria: { false: "No", true: "Yes" } + }) + } + }) + const { answers } = yield* DecisionModel.decide(definition, { input: "Help" }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.gen(function*() { + const { questions } = yield* getRequestBody(request) + + assert.strictEqual(Object.getPrototypeOf(questions), Object.prototype) + assert.isTrue(Object.hasOwn(questions, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(questions, key)?.enumerable) + assert.deepStrictEqual(Object.keys(questions), [key]) + assert.deepStrictEqual(questions[key], { + type: "noul", + instructions: "Needs action now", + criteria: { false: "No", true: "Yes" } + }) + + return jsonResponse(request, { + model: "test/decision-model", + answers: { [key]: { type: "noul", noul: 0.75 } }, + usage: { input_tokens: 20, output_tokens: 4 } + }) + }) + )) + ) + + assert.strictEqual(Object.getPrototypeOf(answers), null) + assert.isTrue(Object.hasOwn(answers, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(answers, key)?.enumerable) + assert.deepStrictEqual(Object.keys(answers), [key]) + assert.deepStrictEqual(answers[key], { probability: 0.75 }) + })) + } + + for ( + const apiUrl of [ + "https://proxy.test/openrouter/v1", + "https://proxy.test/openrouter/v1/", + "https://proxy.test/openrouter", + "https://proxy.test/openrouter/" + ] + ) { + it.effect("resolves the alpha endpoint for " + apiUrl, () => + Effect.gen(function*() { + const requests: Array = [] + yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, decisionsResponse)) + }, apiUrl)) + ) + assert.strictEqual(requests.length, 1) + assert.strictEqual(requests[0].url, "https://proxy.test/openrouter/alpha/decisions") + })) + } + + it.effect("applies a scoped client transform only to the enclosed decisions request", () => + Effect.gen(function*() { + const requests: Array = [] + yield* Effect.gen(function*() { + yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + OpenRouterConfig.withClientTransform( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-decision-scope", "scoped")) + ) + ) + yield* DecisionModel.decide(TicketTriage, { input: ticket }) + }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, decisionsResponse)) + })) + ) + assert.strictEqual(requests.length, 2) + assert.strictEqual(requests[0].headers["x-decision-scope"], "scoped") + assert.isUndefined(requests[1].headers["x-decision-scope"]) + })) + + it.effect("rejects numeric state before making an HTTP request", () => + Effect.gen(function*() { + const definition = Decision.make({ input: Schema.Number, decisions: TicketTriage.decisions }) + const error = yield* DecisionModel.decide(definition, { input: 42 }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer(() => Effect.die(new Error("Unexpected HTTP request")))), + Effect.flip + ) + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidUserInputError") + })) + + it.effect("maps a malformed successful response to a client InvalidOutputError", () => + Effect.gen(function*() { + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...decisionsResponse, + usage: { input_tokens: "not a number", output_tokens: 48 } + })) + )), + Effect.flip + ) + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.module, "OpenRouterClient") + assert.strictEqual(error.method, "createDecisions") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("make constructs a DecisionModel service", () => + Effect.gen(function*() { + const service = yield* OpenRouterDecisionModel.make({ model: "test/decision-model" }) + assert.isFunction(service.decide) + }).pipe(Effect.provide(makeClientLayer(() => Effect.die(new Error("Unexpected HTTP request")))))) + + it.effect("forwards request config and scoped overrides", () => + Effect.gen(function*() { + const requests: Array = [] + yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provideService(OpenRouterDecisionModel.Config, { user: "scoped-user", trace: { trace_id: "scoped" } }), + Effect.provide(OpenRouterDecisionModel.layer({ + model: "test/decision-model", + config: { + provider: { order: ["test-provider"] }, + session_id: "session-1", + user: "default-user", + trace: { trace_id: "default" } + } + })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, decisionsResponse)) + })) + ) + const body = yield* getRequestBody(requests[0]) + assert.deepStrictEqual(body.provider, { order: ["test-provider"] }) + assert.strictEqual(body.session_id, "session-1") + assert.strictEqual(body.user, "scoped-user") + assert.deepStrictEqual(body.trace, { trace_id: "scoped" }) + })) + + it.effect("accepts absent confidence, cost, id and provider without inventing values", () => + Effect.gen(function*() { + const { confidence: _choiceConfidence, ...department } = decisionsResponse.answers.department + const { confidence: _scoreConfidence, legend: _legend, ...frustration } = decisionsResponse.answers.frustration + const { answers, usage } = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...decisionsResponse, + answers: { ...decisionsResponse.answers, department, frustration } + })) + )) + ) + assert.isUndefined(answers.department.confidence) + assert.isUndefined(answers.frustration.confidence) + assert.strictEqual(answers.frustration.label, "Very angry, strong language") + assert.strictEqual(answers.urgent.probability, 0.999) + assert.deepStrictEqual({ ...usage }, { inputTokens: 312, outputTokens: 48 }) + })) + + it.effect("drops provider-specific cost, id and provider metadata", () => + Effect.gen(function*() { + const result = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...decisionsResponse, + id: "decision-1", + provider: "test-provider", + usage: { ...decisionsResponse.usage, cost: 0.001 } + })) + )) + ) + assert.deepStrictEqual({ ...result.usage }, { inputTokens: 312, outputTokens: 48 }) + assert.isFalse("id" in result) + assert.isFalse("provider" in result) + assert.isFalse("cost" in result.answers.department) + })) + + it.effect("fails with InvalidOutputError when a choice answer omits probabilities", () => + Effect.gen(function*() { + const { probabilities: _probabilities, ...department } = decisionsResponse.answers.department + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...decisionsResponse, + answers: { ...decisionsResponse.answers, department } + })) + )), + Effect.flip + ) + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("model provides DecisionModel with openrouter provider metadata", () => + Effect.gen(function*() { + const provider = yield* Model.ProviderName + const modelName = yield* Model.ModelName + const model = yield* DecisionModel.DecisionModel + + assert.strictEqual(provider, "openrouter") + assert.strictEqual(modelName, "test/decision-model") + assert.isDefined(model.decide) + }).pipe( + Effect.provide(OpenRouterDecisionModel.model("test/decision-model")), + Effect.provide(makeClientLayer(() => Effect.die(new Error("Unexpected HTTP request")))) + )) + + it.effect("sends the encoded input as state and every decision as a question in one request", () => + Effect.gen(function*() { + const requests: Array = [] + const clientLayer = makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, decisionsResponse)) + }) + + yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(clientLayer) + ) + + assert.strictEqual(requests.length, 1) + assert.strictEqual(requests[0].method, "POST") + assert.strictEqual(requests[0].url, "https://openrouter.ai/api/alpha/decisions") + assert.strictEqual(requests[0].headers["authorization"], "Bearer or-test-key") + + assert.strictEqual(requests[0].headers["http-referer"], "https://example.com") + assert.strictEqual(requests[0].headers["x-title"], "Decision contract tests") + + const body = yield* getRequestBody(requests[0]) + assert.deepStrictEqual(body, { + state: { + message: ticket.message, + orderId: "A-104" + }, + model: "test/decision-model", + questions: { + department: { + type: "choice", + instructions: "Which team should handle this", + criteria: { + billing: "Payment or subscription issues", + technical: "Bugs or integration problems", + sales: "Pricing or account questions" + } + }, + frustration: { + type: "score", + instructions: "How frustrated the customer appears", + criteria: ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"] + }, + urgent: { + type: "noul", + instructions: "The message conveys urgency or time-sensitivity", + criteria: { + false: "No time pressure", + true: "Needs action now" + } + } + } + }) + })) + + it.effect("maps choice, score, and noul answers onto classify, rate, and probability answers", () => + Effect.gen(function*() { + const { answers, usage } = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => Effect.succeed(jsonResponse(request, decisionsResponse)))) + ) + + assert.strictEqual(answers.department.label, "billing") + assert.deepStrictEqual(answers.department.probabilities, { billing: 0.84, technical: 0.159, sales: 0.001 }) + assert.strictEqual(answers.department.confidence, 0.596) + + assert.strictEqual(answers.frustration.rating, 1.6) + assert.strictEqual(answers.frustration.label, "Very angry, strong language") + assert.deepStrictEqual(answers.frustration.probabilities, { + "Calm, just stating facts": 0.05, + "Frustrated but civil": 0.3, + "Very angry, strong language": 0.65 + }) + assert.strictEqual(answers.frustration.confidence, 0.78) + + assert.strictEqual(answers.urgent.probability, 0.999) + assert.isFalse("confidence" in answers.urgent) + + assert.strictEqual(usage.inputTokens, 312) + assert.strictEqual(usage.outputTokens, 48) + })) + + it.effect("encodes an explicitly undefined optional input field as JSON null", () => + Effect.gen(function*() { + const requests: Array = [] + const definition = Decision.make({ + input: Schema.Struct({ message: Schema.String, orderId: Schema.optional(Schema.String) }), + decisions: TicketTriage.decisions + }) + const { answers } = yield* DecisionModel.decide(definition, { + input: { message: ticket.message, orderId: undefined } + }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, decisionsResponse)) + })) + ) + + assert.strictEqual(requests.length, 1) + const body = yield* getRequestBody(requests[0]) + assert.deepStrictEqual(body.state, { message: ticket.message, orderId: null }) + assert.strictEqual(answers.urgent.probability, 0.999) + })) + + it.effect("sends a pinned model id through unchanged", () => + Effect.gen(function*() { + const requests: Array = [] + + const modelName = yield* Effect.gen(function*() { + yield* DecisionModel.decide(TicketTriage, { input: ticket }) + return yield* Model.ModelName + }).pipe( + Effect.provide(OpenRouterDecisionModel.model("test/decision-model-pinned")), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, { ...decisionsResponse, model: "test/decision-model-pinned" })) + })) + ) + + const body = yield* getRequestBody(requests[0]) + assert.strictEqual(modelName, "test/decision-model-pinned") + assert.strictEqual(body.model, "test/decision-model-pinned") + })) + + it.effect("passes a string input through as a string state", () => + Effect.gen(function*() { + const requests: Array = [] + const Sentiment = Decision.make({ + input: Schema.String, + decisions: { + urgent: Decision.probability({ + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + }) + } + }) + + yield* DecisionModel.decide(Sentiment, { input: "My card was charged twice." }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, { + model: "test/decision-model", + answers: { urgent: { type: "noul", noul: 0.93 } }, + usage: { input_tokens: 20, output_tokens: 4 } + })) + })) + ) + + const body = yield* getRequestBody(requests[0]) + assert.strictEqual(body.state, "My card was charged twice.") + })) + + it.effect("passes an array input through as an array state", () => + Effect.gen(function*() { + const requests: Array = [] + const Conversation = Decision.make({ + input: Schema.Array(Schema.String), + decisions: { + urgent: Decision.probability({ + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + }) + } + }) + + yield* DecisionModel.decide(Conversation, { + input: ["Hi", "My customer number is TS1337.", "My card was charged twice."] + }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, { + model: "test/decision-model", + answers: { urgent: { type: "noul", noul: 0.93 } }, + usage: { input_tokens: 20, output_tokens: 4 } + })) + })) + ) + + const body = yield* getRequestBody(requests[0]) + assert.deepStrictEqual(body.state, ["Hi", "My customer number is TS1337.", "My card was charged twice."]) + })) + + it.effect("fails with InvalidOutputError when an answer type does not match the decision", () => + Effect.gen(function*() { + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...decisionsResponse, + answers: { + ...decisionsResponse.answers, + urgent: { + type: "choice", + choice: "true", + probabilities: { true: 0.9, false: 0.1 }, + confidence: 0.8 + } + } + })) + )), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when an answer is missing", () => + Effect.gen(function*() { + const { urgent: _urgent, ...answers } = decisionsResponse.answers + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide( + makeClientLayer((request) => Effect.succeed(jsonResponse(request, { ...decisionsResponse, answers }))) + ), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when a score answer omits probabilities", () => + Effect.gen(function*() { + const { probabilities: _probabilities, ...frustration } = decisionsResponse.answers.frustration + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...decisionsResponse, + answers: { ...decisionsResponse.answers, frustration } + })) + )), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when a score distribution skips a level index", () => + Effect.gen(function*() { + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...decisionsResponse, + answers: { + ...decisionsResponse.answers, + frustration: { + ...decisionsResponse.answers.frustration, + probabilities: { "0": 0.35, "2": 0.65 } + } + } + })) + )), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + for ( + const [status, reason] of [ + [400, "InvalidRequestError"], + [401, "AuthenticationError"], + [402, "UnknownError"], + [403, "AuthenticationError"], + [404, "InvalidRequestError"], + [413, "UnknownError"], + [429, "RateLimitError"], + [500, "InternalProviderError"], + [502, "InternalProviderError"], + [503, "InternalProviderError"], + [524, "InternalProviderError"], + [529, "InternalProviderError"] + ] as const + ) { + it.effect("maps HTTP " + status + " using the existing OpenRouter error mapping", () => + Effect.gen(function*() { + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(HttpClientResponse.fromWeb( + request, + new Response( + JSON.stringify({ error: { message: "Decision request failed", code: status } }), + { status, headers: { "content-type": "application/json", "retry-after": "3" } } + ) + )) + )), + Effect.flip + ) + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, reason) + })) + } +}) + +const makeClientLayer = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect, + apiUrl?: string +) => + OpenRouterClient.layer({ + apiUrl, + apiKey: Redacted.make("or-test-key"), + siteReferrer: "https://example.com", + siteTitle: "Decision contract tests" + }).pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, makeHttpClient(handler))) + ) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return yield* handler(request) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const jsonResponse = ( + request: HttpClientRequest.HttpClientRequest, + body: unknown +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + status: 200, + headers: { + "content-type": "application/json" + } + }) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts b/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts index ab6738fdd..1fa775be2 100644 --- a/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts +++ b/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts @@ -6,6 +6,49 @@ import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("OpenRouterLanguageModel", () => { + describe("strictJsonSchema", () => { + it.effect("omits false from requests while preserving response strictness", () => + Effect.gen(function*() { + yield* LanguageModel.generateObject({ + prompt: "Give me a name", + schema: Schema.Struct({ name: Schema.String }) + }).pipe(Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini", { strictJsonSchema: false }))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + strictEqual(body.response_format.json_schema.strict, false) + assert.notProperty(body, "strictJsonSchema") + }).pipe(Effect.provide(makeTestLayer({ + body: { + choices: [{ + finish_reason: "stop", + index: 0, + message: { role: "assistant", content: JSON.stringify({ name: "Alice" }) } + }] + } + })))) + + it.effect("omits true from streaming requests while preserving tool strictness", () => + Effect.gen(function*() { + const tool = Tool.make("FlexibleTool", { parameters: Schema.Struct({ query: Schema.String }) }) + .annotate(Tool.Strict, false) + yield* LanguageModel.streamText({ + prompt: "Use a tool", + toolkit: Toolkit.make(tool), + disableToolCallResolution: true + }).pipe( + Stream.runDrain, + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini", { strictJsonSchema: true })) + ) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + strictEqual(body.stream, true) + strictEqual(body.tools[0].function.strict, false) + assert.notProperty(body, "strictJsonSchema") + }).pipe(Effect.provide(makeStreamTestLayer([])))) + }) + describe("generateText", () => { describe("message preparation", () => { describe("audio file parts", () => { @@ -171,6 +214,42 @@ describe("OpenRouterLanguageModel", () => { }]) }).pipe(Effect.provide(makeTestLayer()))) }) + + it.effect("preserves string tool results", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: [Prompt.toolCallPart({ + id: "call_text", + name: "text_tool", + params: {}, + providerExecuted: false + })] + }, + { + role: "tool", + content: [Prompt.toolResultPart({ + id: "call_text", + name: "text_tool", + result: "PLAIN_TEXT_SENTINEL\n", + isFailure: false, + providerExecuted: false + })] + } + ]), + disableToolCallResolution: true + }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const toolResult = body.messages.find((message: any) => message.role === "tool") + + assert.isDefined(toolResult) + strictEqual(toolResult.content, "PLAIN_TEXT_SENTINEL\n") + }).pipe(Effect.provide(makeTestLayer()))) }) describe("tool preparation", () => { @@ -208,9 +287,129 @@ describe("OpenRouterLanguageModel", () => { deepStrictEqual(tool.function.parameters, inputSchema) }).pipe(Effect.provide(makeTestLayer()))) }) + + describe("usage", () => { + it.effect("derives text and uncached tokens when details are subsets of their totals", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ prompt: "Hello" }).pipe( + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini")) + ) + + deepStrictEqual( + result.usage.inputTokens, + { uncached: 70, total: 100, cacheRead: 30, cacheWrite: 0 }, + "subset input usage" + ) + deepStrictEqual(result.usage.outputTokens, { total: 50, text: 30, reasoning: 20 }, "subset output usage") + }).pipe(Effect.provide(makeTestLayer({ + body: { + usage: { + prompt_tokens: 100, + prompt_tokens_details: { cached_tokens: 30 }, + completion_tokens: 50, + completion_tokens_details: { reasoning_tokens: 20 }, + total_tokens: 150 + } + } + })))) + + it.effect("preserves totals when detail counts equal their parent counts", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ prompt: "Hello" }).pipe( + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini")) + ) + + deepStrictEqual( + result.usage.inputTokens, + { uncached: 0, total: 100, cacheRead: 100, cacheWrite: 0 }, + "input usage at equality" + ) + deepStrictEqual(result.usage.outputTokens, { total: 20, text: 0, reasoning: 20 }, "output usage at equality") + }).pipe(Effect.provide(makeTestLayer({ + body: { + usage: { + prompt_tokens: 100, + prompt_tokens_details: { cached_tokens: 100 }, + completion_tokens: 20, + completion_tokens_details: { reasoning_tokens: 20 }, + total_tokens: 120 + } + } + })))) + + it.effect("treats reasoning tokens as disjoint when they exceed completion tokens", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ prompt: "Hello" }).pipe( + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini")) + ) + + deepStrictEqual(result.usage.outputTokens, { total: 30, text: 10, reasoning: 20 }, "disjoint reasoning usage") + }).pipe(Effect.provide(makeTestLayer({ + body: { + usage: { + prompt_tokens: 100, + completion_tokens: 10, + completion_tokens_details: { reasoning_tokens: 20 }, + total_tokens: 110 + } + } + })))) + + it.effect("treats cached tokens as disjoint when they exceed prompt tokens", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ prompt: "Hello" }).pipe( + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini")) + ) + + deepStrictEqual( + result.usage.inputTokens, + { uncached: 100, total: 400, cacheRead: 300, cacheWrite: 0 }, + "disjoint cached usage" + ) + }).pipe(Effect.provide(makeTestLayer({ + body: { + usage: { + prompt_tokens: 100, + prompt_tokens_details: { cached_tokens: 300 }, + completion_tokens: 10, + total_tokens: 110 + } + } + })))) + }) }) describe("streamText", () => { + describe("usage", () => { + it.effect("treats streamed reasoning tokens as disjoint when they exceed completion tokens", () => + Effect.gen(function*() { + const parts = yield* LanguageModel.streamText({ prompt: "Hello" }).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: [], + usage: { + prompt_tokens: 100, + completion_tokens: 10, + completion_tokens_details: { reasoning_tokens: 20 }, + total_tokens: 110 + } + }])) + ) + + const finishPart = parts.find((part) => part.type === "finish") + deepStrictEqual( + finishPart?.usage.outputTokens, + { total: 30, text: 10, reasoning: 20 }, + "streamed disjoint reasoning usage" + ) + })) + }) + it.effect("preserves streamed citation start and end indexes", () => Effect.gen(function*() { const parts = yield* LanguageModel.streamText({ prompt: "cite a source" }).pipe( @@ -439,20 +638,27 @@ const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => 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 - ) + const httpClientLayer = Layer.effectContext(Effect.gen(function*() { + const capturedRequests = yield* Ref.make>([]) + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + yield* Ref.update(capturedRequests, Array.append(request)) + return HttpClientResponse.fromWeb( + request, + new Response(body, { + status: 200, + headers: { "content-type": "text/event-stream" } + }) + ) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + return Context.make(HttpClient.HttpClient, httpClient).pipe( + Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) })) + ) + })) return OpenRouterClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( - Layer.provide(Layer.succeed(HttpClient.HttpClient, httpClient)) + Layer.provideMerge(httpClientLayer) ) } diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterSchema.test.ts b/repos/effect/packages/ai/openrouter/test/OpenRouterSchema.test.ts new file mode 100644 index 000000000..6e05c34c1 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/test/OpenRouterSchema.test.ts @@ -0,0 +1,144 @@ +import * as OpenRouterSchema from "@effect/ai-openrouter/OpenRouterSchema" +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" + +const choiceAnswer = { + type: "choice", + choice: "billing", + probabilities: { billing: 0.84, technical: 0.159, sales: 0.001 }, + confidence: 0.596 +} + +const scoreAnswer = { + type: "score", + score: 1.6, + legend: { "0": "Calm", "1": "Frustrated", "2": "Very angry" }, + probabilities: { "0": 0.05, "1": 0.3, "2": 0.65 }, + confidence: 0.78 +} + +const noulAnswer = { type: "noul", noul: 0.999 } + +describe("OpenRouterSchema", () => { + it("encodes a decisions request with every question type", () => { + const request = { + model: "openai/gpt-4o-mini", + state: { message: "My card was charged twice.", orderId: "A-104" }, + questions: { + department: { + type: "choice", + instructions: "Which team should handle this", + criteria: { billing: "Payment issues", technical: "Bugs" } + }, + frustration: { + type: "score", + instructions: "How frustrated the customer appears", + criteria: ["Calm", "Frustrated", "Very angry"] + }, + urgent: { + type: "noul", + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + } + } + } as const + + const decoded = Schema.decodeUnknownSync(OpenRouterSchema.DecisionsRequest)(request) + const encoded = Schema.encodeSync(OpenRouterSchema.DecisionsRequest)(decoded) + + assert.deepStrictEqual(JSON.parse(JSON.stringify(encoded)), request) + }) + + it("accepts a request carrying routing and tracing options", () => { + const decoded = Schema.decodeUnknownSync(OpenRouterSchema.DecisionsRequest)({ + model: "openai/gpt-4o-mini", + state: "hello", + questions: { urgent: { type: "noul", instructions: "Urgent" } }, + provider: { order: ["openai"] }, + session_id: "session-1", + user: "user-1" + }) + + assert.strictEqual(decoded.session_id, "session-1") + assert.strictEqual(decoded.user, "user-1") + }) + + it("accepts a noul question without criteria", () => { + const decoded = Schema.decodeUnknownSync(OpenRouterSchema.NoulQuestion)({ + type: "noul", + instructions: "The message conveys urgency" + }) + + assert.strictEqual(decoded.type, "noul") + assert.isUndefined(decoded.criteria) + }) + + it.each([ + { name: "string", state: "a plain string" }, + { name: "object", state: { message: "hi", orderId: "A-1" } }, + { name: "array", state: ["first", "second"] } + ])("accepts a $name state", ({ state }) => { + const decoded = Schema.decodeUnknownSync(OpenRouterSchema.DecisionsRequest)({ + model: "openai/gpt-4o-mini", + state, + questions: { urgent: { type: "noul", instructions: "Urgent" } } + }) + + assert.deepStrictEqual(decoded.state, state) + }) + + it("decodes a response with choice, score and noul answers", () => { + const response = { + id: "dec_123", + model: "openai/gpt-4o-mini", + provider: "OpenAI", + answers: { department: choiceAnswer, frustration: scoreAnswer, urgent: noulAnswer }, + usage: { input_tokens: 312, output_tokens: 48, cost: 0.00042 } + } + + const decoded = Schema.decodeUnknownSync(OpenRouterSchema.DecisionsResponse)(response) + + assert.deepStrictEqual(JSON.parse(JSON.stringify(decoded)), response) + }) + + it("decodes a response without id, provider or cost", () => { + const decoded = Schema.decodeUnknownSync(OpenRouterSchema.DecisionsResponse)({ + model: "openai/gpt-4o-mini", + answers: { urgent: noulAnswer }, + usage: { input_tokens: 10, output_tokens: 2 } + }) + + assert.isUndefined(decoded.id) + assert.isUndefined(decoded.provider) + assert.isUndefined(decoded.usage.cost) + }) + + it.each([ + { name: "choice answer without probabilities", answer: { type: "choice", choice: "billing" } }, + { name: "choice answer without confidence", answer: { type: "choice", choice: "billing", probabilities: {} } }, + { name: "score answer without probabilities", answer: { type: "score", score: 1.6 } }, + { name: "score answer without legend", answer: { type: "score", score: 1.6, probabilities: { "0": 1 } } } + ])("decodes a $name, since the field is optional upstream", ({ answer }) => { + const decoded = Schema.decodeUnknownSync(OpenRouterSchema.Answer)(answer) + + assert.strictEqual(decoded.type, (answer as { type: string }).type) + }) + + it.each([ + { name: "choice answer without choice", answer: { type: "choice", probabilities: { billing: 1 } } }, + { name: "score answer without score", answer: { type: "score", probabilities: { "0": 1 } } }, + { name: "noul answer without noul", answer: { type: "noul" } }, + { name: "answer with an unknown type", answer: { type: "rank", rank: [] } } + ])("rejects a $name", ({ answer }) => { + assert.throws(() => Schema.decodeUnknownSync(OpenRouterSchema.Answer)(answer)) + }) + + it("rejects a response without usage", () => { + assert.throws(() => + Schema.decodeUnknownSync(OpenRouterSchema.DecisionsResponse)({ + model: "openai/gpt-4o-mini", + answers: { urgent: noulAnswer } + }) + ) + }) +}) diff --git a/repos/effect/packages/ai/openrouter/typetest/OpenRouterDecisionModel.tst.ts b/repos/effect/packages/ai/openrouter/typetest/OpenRouterDecisionModel.tst.ts new file mode 100644 index 000000000..8a809d756 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/typetest/OpenRouterDecisionModel.tst.ts @@ -0,0 +1,69 @@ +import { type OpenRouterClient, OpenRouterDecisionModel } from "@effect/ai-openrouter" +import { Effect, type Layer, Schema } from "effect" +import { Decision, DecisionModel, type Model } from "effect/unstable/ai" +import { describe, expect, it } from "tstyche" + +const Triage = Decision.make({ + input: Schema.String, + decisions: { + department: Decision.classify({ + instructions: "Which team should handle this", + criteria: { billing: "Payments", technical: "Bugs" } + }), + urgent: Decision.probability({ + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + }) + } +}) + +describe("OpenRouterDecisionModel", () => { + it("model provides DecisionModel and requires OpenRouterClient", () => { + expect(OpenRouterDecisionModel.model("openai/gpt-4o-mini")).type.toBe< + Model.Model<"openrouter", DecisionModel.DecisionModel, OpenRouterClient.OpenRouterClient> + >() + }) + + it("layer provides DecisionModel and requires OpenRouterClient", () => { + expect(OpenRouterDecisionModel.layer({ model: "openai/gpt-4o-mini" })).type.toBe< + Layer.Layer + >() + }) + + it("make builds a DecisionModel from the client service", () => { + expect(OpenRouterDecisionModel.make({ model: "openai/gpt-4o-mini" })).type.toBe< + Effect.Effect + >() + }) + + it("accepts any model id, since OpenRouter ids are namespaced strings", () => { + expect(OpenRouterDecisionModel.model).type.toBeCallableWith("anthropic/claude-sonnet-4") + expect(OpenRouterDecisionModel.model).type.toBeCallableWith("some-new-provider/some-new-model") + }) + + it("satisfies a decide program's DecisionModel requirement", () => { + const program = DecisionModel.decide(Triage, { input: "hello" }).pipe( + Effect.provide(OpenRouterDecisionModel.model("openai/gpt-4o-mini")) + ) + + type Requirements = typeof program extends Effect.Effect ? R : never + + expect().type.toBe() + }) + + it("accepts routing and tracing options in the layer config", () => { + expect(OpenRouterDecisionModel.layer).type.toBeCallableWith({ + model: "openai/gpt-4o-mini", + config: { session_id: "session-1", user: "user-1" } + }) + }) + + it("exposes Config as a context service whose fields are all optional", () => { + expect<{ readonly user: string }>().type.toBeAssignableTo< + typeof OpenRouterDecisionModel.Config.Service + >() + expect<{ readonly session_id: string }>().type.toBeAssignableTo< + typeof OpenRouterDecisionModel.Config.Service + >() + }) +}) diff --git a/repos/effect/packages/ai/typesafe/CHANGELOG.md b/repos/effect/packages/ai/typesafe/CHANGELOG.md new file mode 100644 index 000000000..e7f74e183 --- /dev/null +++ b/repos/effect/packages/ai/typesafe/CHANGELOG.md @@ -0,0 +1,16 @@ +# @effect/ai-typesafe + +## 4.0.0-rc.117 + +### Patch Changes + +- Updated dependencies [[`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9), [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724), [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0), [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b), [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9), [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a), [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5), [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741)]: + - effect@4.0.0-rc.117 + +## 4.0.0-rc.116 + +### Patch Changes + +- [#8287](https://github.com/Effect-TS/effect/pull/8287) [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2) Thanks @tim-smart! - Add `@effect/ai-typesafe`, a `DecisionModel` provider for TypeSafe's System One API. +- Updated dependencies [[`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f), [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2), [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d), [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356), [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf), [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b), [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f), [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a), [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac), [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06), [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8), [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153), [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537), [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746), [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9), [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c), [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3), [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b), [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f), [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5), [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55), [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b)]: + - effect@4.0.0-rc.116 diff --git a/repos/effect/packages/ai/typesafe/README.md b/repos/effect/packages/ai/typesafe/README.md new file mode 100644 index 000000000..6f22fecd9 --- /dev/null +++ b/repos/effect/packages/ai/typesafe/README.md @@ -0,0 +1,7 @@ +# @effect/ai-typesafe + +TypeSafe System One provider for Effect's `DecisionModel`. Supports classification, ordered ratings, and probabilities through Effect `HttpClient`. + +Use `TypeSafeClient.layerConfig()` to read `TYPESAFE_API_KEY`, then provide `TypeSafeDecisionModel.model("jev-latest")` to a decision workflow. Versioned model identifiers such as `jev-1.13.0` are also supported. + +The client does not retry automatically. Rate-limit errors include the provider's retry delay when available. Provider distributions are preserved, not normalized; they must satisfy the core `DecisionModel` validation, including its current `1e-6` sum tolerance. TypeSafe's rounding behavior has not been verified against the live API. diff --git a/repos/effect/packages/ai/typesafe/package.json b/repos/effect/packages/ai/typesafe/package.json new file mode 100644 index 000000000..f3150394b --- /dev/null +++ b/repos/effect/packages/ai/typesafe/package.json @@ -0,0 +1,69 @@ +{ + "name": "@effect/ai-typesafe", + "version": "4.0.0-rc.117", + "type": "module", + "license": "MIT", + "description": "A TypeSafe DecisionModel provider for Effect", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/typesafe" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "ai", + "typesafe" + ], + "keywords": [ + "typescript", + "ai", + "typesafe" + ], + "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" + }, + "devDependencies": { + "effect": "workspace:^" + }, + "peerDependencies": { + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/ai/typesafe/src/TypeSafeClient.ts b/repos/effect/packages/ai/typesafe/src/TypeSafeClient.ts new file mode 100644 index 000000000..f480682e0 --- /dev/null +++ b/repos/effect/packages/ai/typesafe/src/TypeSafeClient.ts @@ -0,0 +1,143 @@ +/** + * HTTP client for TypeSafe System One and model discovery. + * + * @since 4.0.0 + */ +import * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { flow, identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Redacted from "effect/Redacted" +import type * as Schema from "effect/Schema" +import * as AiError from "effect/unstable/ai/AiError" +import * as HttpClient from "effect/unstable/http/HttpClient" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as Errors from "./internal/errors.ts" +import { TypeSafeConfig } from "./TypeSafeConfig.ts" +import * as TypeSafeSchema from "./TypeSafeSchema.ts" + +/** + * Low-level TypeSafe operations. + * + * @category services + * @since 4.0.0 + */ +export interface Service { + readonly client: HttpClient.HttpClient + readonly systemOne: ( + request: typeof TypeSafeSchema.SystemOneRequest.Encoded + ) => Effect.Effect + readonly listModels: () => Effect.Effect +} + +/** + * TypeSafe client service. + * + * @category services + * @since 4.0.0 + */ +export class TypeSafeClient extends Context.Service()("@effect/ai-typesafe/TypeSafeClient") {} + +/** + * Authentication, endpoint and HTTP customization options. + * + * @category options + * @since 4.0.0 + */ +export interface Options { + readonly apiKey?: Redacted.Redacted | undefined + readonly apiUrl?: string | undefined + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +} + +const decodeSystemOne = HttpClientResponse.schemaBodyJson(TypeSafeSchema.SystemOneResponse) +const decodeListModels = HttpClientResponse.schemaBodyJson(TypeSafeSchema.ListModelsResponse) + +/** + * Builds a client without automatic retries. Scoped transforms run after the constructor transform. + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced( + function*(options: Options): Effect.fn.Return { + const base = yield* HttpClient.HttpClient + const client = base.pipe( + HttpClient.mapRequest(flow( + HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.typesafe.ai/v1"), + options.apiKey ? HttpClientRequest.bearerToken(Redacted.value(options.apiKey)) : identity, + HttpClientRequest.acceptJson + )), + HttpClient.filterStatusOk, + options.transformClient ?? identity + ) + const resolveClient = Effect.map( + TypeSafeConfig.getOrUndefined, + (config) => config?.transformClient?.(client) ?? client + ) + const execute = ( + request: HttpClientRequest.HttpClientRequest, + decode: ( + response: HttpClientResponse.HttpClientResponse + ) => Effect.Effect, + method: string + ) => + resolveClient.pipe( + Effect.flatMap((client) => client.execute(request)), + Effect.flatMap(decode), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, method), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, method)) + }) + ) + return TypeSafeClient.of({ + client, + systemOne: (payload) => + HttpClientRequest.bodyJson(HttpClientRequest.post("/systemone"), payload).pipe( + Effect.mapError((error) => + AiError.make({ + module: "TypeSafeClient", + method: "systemOne", + reason: new AiError.InvalidRequestError({ description: String(error) }) + }) + ), + Effect.flatMap((request) => execute(request, decodeSystemOne, "systemOne")) + ), + listModels: () => execute(HttpClientRequest.get("/models"), decodeListModels, "listModels") + }) + } +) + +/** + * Provides a client from explicit options. + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: Options): Layer.Layer => + Layer.effect(TypeSafeClient, make(options)) + +/** + * Provides a client from configuration, defaulting to TYPESAFE_API_KEY. + * + * @category layers + * @since 4.0.0 + */ +export const layerConfig = (options?: { + readonly apiKey?: Config.Config | undefined> | undefined + readonly apiUrl?: Config.Config | undefined + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => + Layer.effect( + TypeSafeClient, + Effect.gen(function*() { + return yield* make({ + apiKey: yield* (options?.apiKey ?? Config.Redacted("TYPESAFE_API_KEY")), + apiUrl: options?.apiUrl ? yield* options.apiUrl : undefined, + transformClient: options?.transformClient + }) + }) + ) diff --git a/repos/effect/packages/ai/typesafe/src/TypeSafeConfig.ts b/repos/effect/packages/ai/typesafe/src/TypeSafeConfig.ts new file mode 100644 index 000000000..cf3384d4f --- /dev/null +++ b/repos/effect/packages/ai/typesafe/src/TypeSafeConfig.ts @@ -0,0 +1,68 @@ +/** + * Scoped HTTP client customization for TypeSafe requests. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import type { HttpClient } from "effect/unstable/http/HttpClient" + +/** + * Scoped configuration read when executing TypeSafe requests. + * + * @see {@link withClientTransform} for scoping an HTTP client transformation + * + * @category services + * @since 4.0.0 + */ +export class TypeSafeConfig extends Context.Service< + TypeSafeConfig, + TypeSafeConfig.Service +>()("@effect/ai-typesafe/TypeSafeConfig") { + /** + * Reads the current configuration, if present. + * + * @since 4.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + Context.getOrUndefined(TypeSafeConfig) + ) +} + +/** + * Types used by the `TypeSafeConfig` context service. + * + * @since 4.0.0 + */ +export declare namespace TypeSafeConfig { + /** + * Per-request HTTP client configuration. + * + * @category services + * @since 4.0.0 + */ + export interface Service { + readonly transformClient?: ((client: HttpClient) => HttpClient) | undefined + } +} + +/** + * Transforms the TypeSafe HTTP client for requests made by the supplied effect. + * Replaces any existing scoped transform; compose them manually to apply both. + * + * @category configuration + * @since 4.0.0 + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + transformClient: (client: HttpClient) => HttpClient +) => + Effect.flatMap( + TypeSafeConfig.getOrUndefined, + (config) => Effect.provideService(self, TypeSafeConfig, { ...config, transformClient }) + )) diff --git a/repos/effect/packages/ai/typesafe/src/TypeSafeDecisionModel.ts b/repos/effect/packages/ai/typesafe/src/TypeSafeDecisionModel.ts new file mode 100644 index 000000000..083530999 --- /dev/null +++ b/repos/effect/packages/ai/typesafe/src/TypeSafeDecisionModel.ts @@ -0,0 +1,106 @@ +/** + * TypeSafe's System One implementation of DecisionModel. + * + * @since 4.0.0 + */ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as DecisionModel from "effect/unstable/ai/DecisionModel" +import * as AiModel from "effect/unstable/ai/Model" +import { TypeSafeClient } from "./TypeSafeClient.ts" +import type * as TypeSafeSchema from "./TypeSafeSchema.ts" + +/** + * Known TypeSafe model identifiers. Constructors also accept custom identifiers. + * + * @category models + * @since 4.0.0 + */ +export type Model = "jev-latest" | "jev-preview" | "jev-1.13.0" + +/** + * Creates a decision model with TypeSafe provider metadata. + * + * @category constructors + * @since 4.0.0 + */ +export const model = ( + model: Model | (string & {}) +): AiModel.Model<"typesafe", DecisionModel.DecisionModel, TypeSafeClient> => + AiModel.make("typesafe", model, layer({ model })) + +/** + * Builds a decision service. Provider values are preserved without normalization; + * DecisionModel validates distributions and derives rating labels. + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced( + function*( + options: { readonly model: Model | (string & {}) } + ): Effect.fn.Return { + const client = yield* TypeSafeClient + return yield* DecisionModel.make({ + decide: Effect.fnUntraced(function*({ state, decisions }) { + const questions: Record = Object.create(null) + for (const [key, decision] of Object.entries(decisions)) { + switch (decision._tag) { + case "Classify": + questions[key] = { type: "choice", instructions: decision.instructions, criteria: decision.criteria } + break + case "Rate": + questions[key] = { type: "score", instructions: decision.instructions, criteria: decision.criteria } + break + case "Probability": + questions[key] = { type: "noul", instructions: decision.instructions, criteria: decision.criteria } + break + } + } + const response = yield* client.systemOne({ model: options.model, state, questions }) + const answers: Record = Object.create(null) + for (const [key, decision] of Object.entries(decisions)) { + const answer = response.answers[key] + switch (answer?.type) { + case "choice": + answers[key] = { + _tag: "Classify", + label: answer.choice, + probabilities: answer.probabilities, + confidence: answer.confidence + } + break + case "score": { + const levels = decision._tag === "Rate" ? decision.criteria : [] + const probabilities: Record = Object.create(null) + for (let index = 0; index < levels.length; index++) { + const probability = answer.probabilities[String(index)] + if (probability !== undefined) probabilities[levels[index]] = probability + } + answers[key] = { _tag: "Rate", rating: answer.score, probabilities, confidence: answer.confidence } + break + } + case "noul": + answers[key] = { _tag: "Probability", probability: answer.noul } + break + } + } + return { + answers, + usage: { inputTokens: response.usage?.input_tokens, outputTokens: response.usage?.output_tokens } + } + }) + }) + } +) + +/** + * Provides DecisionModel using an existing TypeSafeClient. + * + * @category layers + * @since 4.0.0 + */ +export const layer = ( + options: { readonly model: Model | (string & {}) } +): Layer.Layer => + Layer.effect(DecisionModel.DecisionModel, make(options)) diff --git a/repos/effect/packages/ai/typesafe/src/TypeSafeSchema.ts b/repos/effect/packages/ai/typesafe/src/TypeSafeSchema.ts new file mode 100644 index 000000000..f5cafcc04 --- /dev/null +++ b/repos/effect/packages/ai/typesafe/src/TypeSafeSchema.ts @@ -0,0 +1,134 @@ +/** + * Codecs for the TypeSafe System One API. + * + * @since 4.0.0 + */ +import * as Schema from "effect/Schema" + +/** + * A classification question. + * + * @category schemas + * @since 4.0.0 + */ +export const ChoiceQuestion = Schema.Struct({ + type: Schema.Literal("choice"), + instructions: Schema.String, + criteria: Schema.Record(Schema.String, Schema.String) +}) + +/** + * An ordered rating question. + * + * @category schemas + * @since 4.0.0 + */ +export const ScoreQuestion = Schema.Struct({ + type: Schema.Literal("score"), + instructions: Schema.String, + criteria: Schema.Array(Schema.String) +}) + +/** + * A probability question. + * + * @category schemas + * @since 4.0.0 + */ +export const NoulQuestion = Schema.Struct({ + type: Schema.Literal("noul"), + instructions: Schema.String, + criteria: Schema.optional(Schema.Struct({ false: Schema.String, true: Schema.String })) +}) + +/** + * A System One question. + * + * @category schemas + * @since 4.0.0 + */ +export const Question = Schema.Union([ChoiceQuestion, ScoreQuestion, NoulQuestion]) + +/** + * A batch of questions against encoded state. + * + * @category schemas + * @since 4.0.0 + */ +export const SystemOneRequest = Schema.Struct({ + model: Schema.String, + state: Schema.Json, + questions: Schema.Record(Schema.String, Question) +}) + +/** + * A classification distribution. + * + * @category schemas + * @since 4.0.0 + */ +export const ChoiceAnswer = Schema.Struct({ + type: Schema.Literal("choice"), + choice: Schema.String, + probabilities: Schema.Record(Schema.String, Schema.Number), + confidence: Schema.Number +}) + +/** + * A zero-based rating and index-keyed distribution. + * + * @category schemas + * @since 4.0.0 + */ +export const ScoreAnswer = Schema.Struct({ + type: Schema.Literal("score"), + score: Schema.Number, + probabilities: Schema.Record(Schema.String, Schema.Number), + confidence: Schema.Number, + legend: Schema.optional(Schema.Record(Schema.String, Schema.String)) +}) + +/** + * A Bernoulli probability. + * + * @category schemas + * @since 4.0.0 + */ +export const NoulAnswer = Schema.Struct({ type: Schema.Literal("noul"), noul: Schema.Number }) + +/** + * A System One answer. + * + * @category schemas + * @since 4.0.0 + */ +export const Answer = Schema.Union([ChoiceAnswer, ScoreAnswer, NoulAnswer]) + +/** + * Answers and token usage for a batch. + * + * @category schemas + * @since 4.0.0 + */ +export const SystemOneResponse = Schema.Struct({ + model: Schema.String, + answers: Schema.Record(Schema.String, Answer), + usage: Schema.optional(Schema.Struct({ + input_tokens: Schema.optional(Schema.Number), + output_tokens: Schema.optional(Schema.Number) + })) +}) + +/** + * Available TypeSafe models. + * + * @category schemas + * @since 4.0.0 + */ +export const ListModelsResponse = Schema.Struct({ + models: Schema.Array(Schema.Struct({ + name: Schema.String, + description: Schema.optional(Schema.String), + release_date: Schema.optional(Schema.String) + })) +}) diff --git a/repos/effect/packages/ai/typesafe/src/index.ts b/repos/effect/packages/ai/typesafe/src/index.ts new file mode 100644 index 000000000..39d0be791 --- /dev/null +++ b/repos/effect/packages/ai/typesafe/src/index.ts @@ -0,0 +1,25 @@ +/** + * @since 4.0.0 + */ + +// @barrel + +/** + * @since 4.0.0 + */ +export * as TypeSafeClient from "./TypeSafeClient.ts" + +/** + * @since 4.0.0 + */ +export * as TypeSafeConfig from "./TypeSafeConfig.ts" + +/** + * @since 4.0.0 + */ +export * as TypeSafeDecisionModel from "./TypeSafeDecisionModel.ts" + +/** + * @since 4.0.0 + */ +export * as TypeSafeSchema from "./TypeSafeSchema.ts" diff --git a/repos/effect/packages/ai/typesafe/src/internal/errors.ts b/repos/effect/packages/ai/typesafe/src/internal/errors.ts new file mode 100644 index 000000000..0828ee6ad --- /dev/null +++ b/repos/effect/packages/ai/typesafe/src/internal/errors.ts @@ -0,0 +1,113 @@ +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Num from "effect/Number" +import * as Option from "effect/Option" +import * as Redactable from "effect/Redactable" +import * as Schema from "effect/Schema" +import * as AiError from "effect/unstable/ai/AiError" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" + +const requestDetails = (request: HttpClientRequest.HttpClientRequest) => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +const ErrorBody = Schema.Struct({ + message: Schema.optional(Schema.String), + type: Schema.optional(Schema.String), + code: Schema.optional(Schema.String) +}) + +const decodeErrorBody = Schema.decodeUnknownOption(ErrorBody) + +const nonNegative = (value: string | undefined): number | undefined => { + if (value === undefined) return undefined + const parsed = Option.getOrUndefined(Num.parse(value)) + return parsed !== undefined && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined +} + +const retryAfter = (headers: Readonly>) => { + const ms = nonNegative(headers["retry-after-ms"]) + if (ms !== undefined) return Duration.millis(ms) + const raw = headers["retry-after"] + if (raw === undefined || raw.trim() === "") return undefined + const seconds = nonNegative(raw) + if (seconds !== undefined) return Duration.seconds(seconds) + const date = Date.parse(raw) + return Number.isNaN(date) ? undefined : Duration.millis(Math.max(0, date - Date.now())) +} + +export const mapSchemaError = (error: Schema.SchemaError, method: string): AiError.AiError => + AiError.make({ + module: "TypeSafeClient", + method, + reason: AiError.InvalidOutputError.fromSchemaError(error) + }) + +export const mapHttpClientError = Effect.fnUntraced( + function*(error: HttpClientError.HttpClientError, method: string): Effect.fn.Return { + const source = error.reason + let reason: AiError.AiErrorReason + switch (source._tag) { + case "TransportError": + case "EncodeError": + case "InvalidUrlError": + reason = new AiError.NetworkError({ + reason: source._tag, + description: source.description, + request: requestDetails(source.request) + }) + break + case "DecodeError": + case "EmptyBodyError": + reason = new AiError.InvalidOutputError({ + description: source.description ?? "Failed to decode TypeSafe response" + }) + break + case "StatusCodeError": { + const { request, response } = source + const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed(source.description))) + const parsed = yield* Effect.try(() => JSON.parse(body ?? "")).pipe(Effect.orElseSucceed(() => undefined)) + const details = Option.getOrUndefined(decodeErrorBody(parsed)) + const http = { + request: requestDetails(request), + response: { status: response.status, headers: Redactable.redact(response.headers) as Record }, + body + } + const description = AiError.buildErrorDescription({ + status: response.status, + method: request.method, + url: request.url, + body, + message: details?.message, + errorCode: details?.code, + errorType: details?.type, + requestId: response.headers["x-typesafe-request-id"] + }) + if (response.status === 429) { + reason = new AiError.RateLimitError({ + retryAfter: retryAfter(response.headers), + http, + metadata: { + typesafe: { + requestId: response.headers["x-typesafe-request-id"] ?? null, + errorCode: details?.code ?? null, + errorType: details?.type ?? null + } + } + }) + } else if (response.status === 404 || response.status === 422) { + reason = new AiError.InvalidRequestError({ description, http }) + } else { + reason = AiError.reasonFromHttpStatus({ status: response.status, description, http }) + } + break + } + } + return yield* AiError.make({ module: "TypeSafeClient", method, reason }) + } +) diff --git a/repos/effect/packages/ai/typesafe/test/TypeSafeClient.test.ts b/repos/effect/packages/ai/typesafe/test/TypeSafeClient.test.ts new file mode 100644 index 000000000..38f5ace3a --- /dev/null +++ b/repos/effect/packages/ai/typesafe/test/TypeSafeClient.test.ts @@ -0,0 +1,568 @@ +import * as TypeSafeClient from "@effect/ai-typesafe/TypeSafeClient" +import * as TypeSafeConfig from "@effect/ai-typesafe/TypeSafeConfig" +import { assert, describe, it } from "@effect/vitest" +import { Config, ConfigProvider, Context, Duration, Effect, Layer, Redacted } 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" +import { vi } from "vitest" +const systemOneRequest = { + state: "My card was charged twice.", + model: "jev-latest", + questions: { + urgent: { + type: "noul", + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + } + } +} as const + +const systemOneBody = { + model: "jev-latest", + answers: { + urgent: { type: "noul", noul: 0.99 } + }, + usage: { input_tokens: 360, output_tokens: 39 } +} + +describe("TypeSafeClient", () => { + describe("make", () => { + it.effect("sets Bearer token from apiKey", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + yield* client.systemOne(systemOneRequest) + + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.headers["authorization"], "Bearer ts-test-12345") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("ts-test-12345") + })))) + + it.effect("posts systemone requests to the default URL as JSON", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const response = yield* client.systemOne(systemOneRequest) + + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.method, "POST") + assert.strictEqual(requests[0]?.url, "https://api.typesafe.ai/v1/systemone") + assert.strictEqual(requests[0]?.headers["content-type"], "application/json") + assert.deepStrictEqual(yield* getRequestBody(requests[0]), systemOneRequest) + assert.strictEqual(response.model, "jev-latest") + assert.deepStrictEqual(response.usage, { input_tokens: 360, output_tokens: 39 }) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("uses custom apiUrl when provided", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + yield* client.systemOne(systemOneRequest) + + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.url, "https://custom.api.com/v2/systemone") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + apiUrl: "https://custom.api.com/v2" + })))) + + it.effect("lists models from the models endpoint", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const response = yield* client.listModels() + + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.method, "GET") + assert.strictEqual(requests[0]?.url, "https://api.typesafe.ai/v1/models") + assert.deepStrictEqual(response.models, [ + { name: "jev-1.13.0", description: "Jev 1.13", release_date: "2026-08-01" } + ]) + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 200, + body: { + models: [{ name: "jev-1.13.0", description: "Jev 1.13", release_date: "2026-08-01" }] + } + })))) + + it.effect("applies transformClient option", () => { + let transformApplied = false + return Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + yield* client.systemOne(systemOneRequest) + 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*() { + const client = yield* TypeSafeClient.TypeSafeClient + yield* client.client.execute(HttpClientRequest.get("/models")) + + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.url, "https://api.typesafe.ai/v1/models") + assert.strictEqual(requests[0]?.headers["authorization"], "Bearer test-key") + assert.strictEqual(requests[0]?.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 TypeSafeConfig transformClient after options transformClient", () => { + let optionsTransformApplied = false + let configTransformApplied = false + + return Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + yield* client.systemOne(systemOneRequest).pipe( + TypeSafeConfig.withClientTransform((client) => { + configTransformApplied = true + return client.pipe( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-typesafe-transform", "config")) + ) + }) + ) + + const requests = yield* MockHttpClient.requests + assert.isTrue(optionsTransformApplied) + assert.isTrue(configTransformApplied) + assert.strictEqual(requests[0]?.headers["x-typesafe-transform"], "config") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => { + optionsTransformApplied = true + return client.pipe( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-typesafe-transform", "options")) + ) + } + }))) + }) + }) + + describe("layer", () => { + it.effect("creates working service", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + assert.isNotNull(client.client) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("layerConfig loads from Config", () => { + const configProvider = ConfigProvider.fromEnv({ + env: { + MY_API_KEY: "ts-config-key", + MY_API_URL: "https://config.api.com/v1" + } + }) + + return Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + yield* client.systemOne(systemOneRequest) + + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.headers["authorization"], "Bearer ts-config-key") + assert.strictEqual(requests[0]?.url, "https://config.api.com/v1/systemone") + }).pipe(Effect.provide(makeConfigTestLayer(configProvider))) + }) + + it.effect("layerConfig defaults to TYPESAFE_API_KEY", () => { + const configProvider = ConfigProvider.fromEnv({ + env: { + TYPESAFE_API_KEY: "ts-env-key" + } + }) + + return Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + yield* client.systemOne(systemOneRequest) + + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.headers["authorization"], "Bearer ts-env-key") + assert.strictEqual(requests[0]?.url, "https://api.typesafe.ai/v1/systemone") + }).pipe(Effect.provide( + TypeSafeClient.layerConfig().pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockTypeSafeResponse, { response: defaultResponse })), + Layer.provide(ConfigProvider.layer(configProvider)) + ) + )) + }) + }) + + describe("request behavior", () => { + it.effect("redacts the authorization header in AI error context", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).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")) + } + assert.strictEqual(String(result.reason.http.request.headers["authorization"]), "") + assert.strictEqual(result.reason.http.response?.status, 422) + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("secret-key") + }, { + status: 422, + body: { message: "questions must not be empty" } + })))) + }) + + describe("error mapping", () => { + it.effect("maps undocumented 409 status to the generic UnknownError fallback", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "UnknownError") + if (result.reason._tag === "UnknownError") { + assert.strictEqual(result.reason.http?.response?.status, 409) + assert.include(result.reason.description ?? "", "Conflict") + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 409, + body: { message: "Conflict" } + })))) + + it.effect("retains 429 diagnostics in provider metadata", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "RateLimitError") + if (result.reason._tag === "RateLimitError") { + assert.deepStrictEqual(result.reason.metadata.typesafe, { + requestId: "req_typesafe_429", + errorCode: "rate_limit_exceeded", + errorType: "requests" + }) + assert.strictEqual(result.reason.http?.response?.status, 429) + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 429, + body: { message: "Slow down", code: "rate_limit_exceeded", type: "requests" }, + headers: { "x-typesafe-request-id": "req_typesafe_429" } + })))) + + describe("HTTP-date Retry-After", { concurrent: false }, () => { + for ( + const { date, expected, name } of [ + { name: "future", date: "Fri, 18 Sep 2026 12:00:30 GMT", expected: 30000 }, + { name: "past", date: "Fri, 18 Sep 2026 11:59:30 GMT", expected: 0 } + ] + ) { + it.effect("honors a " + name + " HTTP-date Retry-After", () => + Effect.gen(function*() { + const now = vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-09-18T12:00:00Z")) + try { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "RateLimitError") + if (result.reason._tag === "RateLimitError") { + assert.isDefined(result.reason.retryAfter) + assert.strictEqual(Duration.toMillis(result.reason.retryAfter!), expected) + } + } finally { + now.mockRestore() + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 429, + body: { message: "Rate limit exceeded" }, + headers: { "retry-after": date } + })))) + } + }) + + it.effect("maps TransportError to NetworkError reason", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.make({ + apiKey: Redacted.make("test-key") + }) + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.module, "TypeSafeClient") + assert.strictEqual(result.method, "systemOne") + 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 client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.module, "TypeSafeClient") + assert.strictEqual(result.method, "systemOne") + assert.strictEqual(result.reason._tag, "InvalidRequestError") + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 400, + body: { message: "Bad request" } + })))) + + it.effect("maps 401 status to AuthenticationError reason", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.reason._tag, "AuthenticationError") + if (result.reason._tag === "AuthenticationError") { + assert.strictEqual(result.reason.kind, "InvalidKey") + assert.include(result.reason.description ?? "", "Invalid API key") + assert.include(result.reason.description ?? "", "req_typesafe") + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 401, + body: { message: "Invalid API key" }, + headers: { "x-typesafe-request-id": "req_typesafe" } + })))) + + it.effect("maps 403 status to AuthenticationError with InsufficientPermissions", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "AuthenticationError") + if (result.reason._tag === "AuthenticationError") { + assert.strictEqual(result.reason.kind, "InsufficientPermissions") + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 403, + body: { message: "Access denied" } + })))) + + it.effect("maps 404 status to InvalidRequestError reason", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "InvalidRequestError") + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 404, + body: { message: "Not found" } + })))) + + it.effect("maps 422 status to InvalidRequestError reason with the message", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "InvalidRequestError") + if (result.reason._tag === "InvalidRequestError") { + assert.include(result.reason.description, "criteria must contain at least two levels") + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 422, + body: { message: "criteria must contain at least two levels" } + })))) + + it.effect("maps 429 status to RateLimitError honoring Retry-After seconds", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "RateLimitError") + if (result.reason._tag === "RateLimitError") { + assert.isDefined(result.reason.retryAfter) + assert.strictEqual(Duration.toMillis(result.reason.retryAfter!), 2000) + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 429, + body: { message: "Rate limit exceeded" }, + headers: { "retry-after": "2" } + })))) + + it.effect("maps 429 status to RateLimitError honoring retry-after-ms", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "RateLimitError") + if (result.reason._tag === "RateLimitError") { + assert.isDefined(result.reason.retryAfter) + assert.strictEqual(Duration.toMillis(result.reason.retryAfter!), 1500) + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 429, + body: { message: "Rate limit exceeded" }, + headers: { "retry-after-ms": "1500" } + })))) + + it.effect("maps 429 status without retry headers to RateLimitError without retryAfter", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "RateLimitError") + if (result.reason._tag === "RateLimitError") { + assert.isUndefined(result.reason.retryAfter) + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 429, + body: { message: "Rate limit exceeded" } + })))) + + it.effect("maps 529 status to InternalProviderError reason", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "InternalProviderError") + if (result.reason._tag === "InternalProviderError") { + assert.strictEqual(result.reason.http?.response?.status, 529) + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 529, + body: { message: "Overloaded" } + })))) + + it.effect("maps 500 status to InternalProviderError reason", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "InternalProviderError") + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 500, + body: { message: "Internal error" } + })))) + + it.effect("maps a malformed success body to InvalidOutputError reason", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.method, "systemOne") + assert.strictEqual(result.reason._tag, "InvalidOutputError") + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 200, + body: { model: "jev-latest", answers: { urgent: { type: "noul" } } } + })))) + + it.effect("maps listModels failures with the listModels method", () => + Effect.gen(function*() { + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.listModels().pipe(Effect.flip) + + assert.strictEqual(result.module, "TypeSafeClient") + assert.strictEqual(result.method, "listModels") + assert.strictEqual(result.reason._tag, "AuthenticationError") + }).pipe(Effect.provide(makeTestLayer(undefined, { + status: 401, + body: { message: "Invalid API key" } + })))) + }) +}) + +interface MockResponse { + readonly status?: number + readonly body: unknown + readonly headers?: Record +} + +class MockTypeSafeResponse extends Context.Service()("MockTypeSafeResponse") {} + +class MockHttpClient extends Context.Service> +}>()("MockHttpClient") { + static requests = MockHttpClient.use((client) => client.requests) +} + +const makeHttpClientContext = Effect.gen(function*() { + const capturedRequests: Array = [] + const mock = yield* MockTypeSafeResponse + + 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 = { + status: 200, + body: systemOneBody +} + +const makeTestLayer = ( + options: TypeSafeClient.Options = { apiKey: Redacted.make("test-key") }, + response: MockResponse = defaultResponse +) => + TypeSafeClient.layer(options).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockTypeSafeResponse, { response })) + ) + +const makeConfigTestLayer = (configProvider: ConfigProvider.ConfigProvider) => + TypeSafeClient.layerConfig({ + apiKey: Config.Redacted("MY_API_KEY"), + apiUrl: Config.String("MY_API_URL") + }).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockTypeSafeResponse, { 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 + ) + +const makeResponse = ( + request: HttpClientRequest.HttpClientRequest, + response: MockResponse +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(response.body), { + status: response.status ?? 200, + headers: { + "content-type": "application/json", + ...response.headers + } + }) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) diff --git a/repos/effect/packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts b/repos/effect/packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts new file mode 100644 index 000000000..a9e7dbab3 --- /dev/null +++ b/repos/effect/packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts @@ -0,0 +1,609 @@ +import { TypeSafeClient, TypeSafeDecisionModel } from "@effect/ai-typesafe" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted, Schema } from "effect" +import { Decision, DecisionModel, Model } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +const Ticket = Schema.Struct({ + message: Schema.String, + orderId: Schema.String +}) + +const TicketTriage = Decision.make({ + input: Ticket, + decisions: { + department: Decision.classify({ + instructions: "Which team should handle this", + criteria: { + billing: "Payment or subscription issues", + technical: "Bugs or integration problems", + sales: "Pricing or account questions" + } + }), + frustration: Decision.rate({ + instructions: "How frustrated the customer appears", + criteria: ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"] + }), + urgent: Decision.probability({ + instructions: "The message conveys urgency or time-sensitivity", + criteria: { + false: "No time pressure", + true: "Needs action now" + } + }) + } +}) + +const ticket = { + message: "Hi, I've been trying to connect my Stripe account for 3 days and it keeps failing. Please help ASAP.", + orderId: "A-104" +} + +const systemOneResponse = { + model: "jev-latest", + answers: { + department: { + type: "choice", + choice: "billing", + probabilities: { billing: 0.84, technical: 0.159, sales: 0.001 }, + confidence: 0.596 + }, + frustration: { + type: "score", + score: 1.6, + legend: { + "0": "Calm, just stating facts", + "1": "Frustrated but civil", + "2": "Very angry, strong language" + }, + probabilities: { "0": 0.05, "1": 0.3, "2": 0.65 }, + confidence: 0.78 + }, + urgent: { + type: "noul", + noul: 0.999 + } + }, + usage: { input_tokens: 312, output_tokens: 48 } +} + +describe("TypeSafeDecisionModel", () => { + it.effect("omits absent probability criteria from the encoded question", () => + Effect.gen(function*() { + const definition = Decision.make({ + input: Schema.String, + decisions: { + urgent: Decision.probability({ instructions: "Needs action now" }) + } + }) + const { answers } = yield* DecisionModel.decide(definition, { input: "Help ASAP" }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => + Effect.gen(function*() { + const { questions } = yield* getRequestBody(request) + assert.deepStrictEqual(questions.urgent, { + type: "noul", + instructions: "Needs action now" + }) + + return jsonResponse(request, { + model: "jev-latest", + answers: { urgent: { type: "noul", noul: 0.75 } }, + usage: { input_tokens: 20, output_tokens: 4 } + }) + }) + )) + ) + + assert.deepStrictEqual(answers.urgent, { probability: 0.75 }) + })) + + for (const key of ["__proto__", "constructor", "toString"]) { + it.effect("round trips a classification label named " + key, () => + Effect.gen(function*() { + const definition = Decision.make({ + input: Schema.String, + decisions: { + category: Decision.classify({ + instructions: "Choose a category", + criteria: { [key]: "Special category", ordinary: "Ordinary category" } + }) + } + }) + const { answers } = yield* DecisionModel.decide(definition, { input: "Help" }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => + Effect.gen(function*() { + const { questions } = yield* getRequestBody(request) + const criteria = questions.category.criteria + + assert.isTrue(Object.hasOwn(criteria, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(criteria, key)?.enumerable) + assert.deepStrictEqual(Object.keys(criteria), [key, "ordinary"]) + assert.strictEqual(criteria[key], "Special category") + assert.strictEqual(Object.getPrototypeOf(criteria), Object.prototype) + + return jsonResponse(request, { + model: "jev-latest", + answers: { + category: { + type: "choice", + choice: key, + probabilities: { [key]: 0.75, ordinary: 0.25 }, + confidence: 0.5 + } + } + }) + }) + )) + ) + const { label, probabilities } = answers.category + + assert.isTrue(Object.hasOwn(probabilities, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(probabilities, key)?.enumerable) + assert.deepStrictEqual(Object.keys(probabilities), [key, "ordinary"]) + assert.deepStrictEqual(probabilities, { [key]: 0.75, ordinary: 0.25 }) + assert.strictEqual(label, key) + assert.strictEqual(Object.getPrototypeOf(probabilities), null) + })) + + it.effect("maps a score distribution to a rate level named " + key, () => + Effect.gen(function*() { + const definition = Decision.make({ + input: Schema.String, + decisions: { + intensity: Decision.rate({ instructions: "How intense", criteria: ["low", key, "high"] }) + } + }) + const { answers } = yield* DecisionModel.decide(definition, { input: "Help" }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => + Effect.gen(function*() { + const { questions } = yield* getRequestBody(request) + assert.deepStrictEqual(questions.intensity.criteria, ["low", key, "high"]) + + return jsonResponse(request, { + model: "jev-latest", + answers: { + intensity: { + type: "score", + score: 1.1, + legend: { "0": "low", "1": key, "2": "high" }, + probabilities: { "0": 0.1, "1": 0.7, "2": 0.2 }, + confidence: 0.5 + } + } + }) + }) + )) + ) + const { label, probabilities, rating } = answers.intensity + + assert.isTrue(Object.hasOwn(probabilities, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(probabilities, key)?.enumerable) + assert.deepStrictEqual(Object.keys(probabilities), ["low", key, "high"]) + assert.deepStrictEqual(probabilities, { low: 0.1, [key]: 0.7, high: 0.2 }) + assert.strictEqual(label, key) + assert.strictEqual(rating, 1.1) + assert.strictEqual(Object.getPrototypeOf(probabilities), null) + })) + + it.effect("sends an own enumerable question named " + key, () => + Effect.gen(function*() { + const definition = Decision.make({ + input: Schema.String, + decisions: { + [key]: Decision.probability({ + instructions: "Needs action now", + criteria: { false: "No", true: "Yes" } + }) + } + }) + const { answers } = yield* DecisionModel.decide(definition, { input: "Help" }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => + Effect.gen(function*() { + const { questions } = yield* getRequestBody(request) + + assert.strictEqual(Object.getPrototypeOf(questions), Object.prototype) + assert.isTrue(Object.hasOwn(questions, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(questions, key)?.enumerable) + assert.deepStrictEqual(Object.keys(questions), [key]) + assert.deepStrictEqual(questions[key], { + type: "noul", + instructions: "Needs action now", + criteria: { false: "No", true: "Yes" } + }) + + return jsonResponse(request, { + model: "jev-latest", + answers: { [key]: { type: "noul", noul: 0.75 } }, + usage: { input_tokens: 20, output_tokens: 4 } + }) + }) + )) + ) + + assert.strictEqual(Object.getPrototypeOf(answers), null) + assert.isTrue(Object.hasOwn(answers, key)) + assert.isTrue(Object.getOwnPropertyDescriptor(answers, key)?.enumerable) + assert.deepStrictEqual(Object.keys(answers), [key]) + assert.deepStrictEqual(answers[key], { probability: 0.75 }) + })) + } + + it.effect("model provides DecisionModel with typesafe provider metadata", () => + Effect.gen(function*() { + const provider = yield* Model.ProviderName + const modelName = yield* Model.ModelName + const model = yield* DecisionModel.DecisionModel + + assert.strictEqual(provider, "typesafe") + assert.strictEqual(modelName, "jev-latest") + assert.isDefined(model.decide) + }).pipe( + Effect.provide(TypeSafeDecisionModel.model("jev-latest")), + Effect.provideService(TypeSafeClient.TypeSafeClient, noopTypeSafeClient) + )) + + it.effect("sends the encoded input as state and every decision as a question in one request", () => + Effect.gen(function*() { + const requests: Array = [] + const clientLayer = makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, systemOneResponse)) + }) + + yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(clientLayer) + ) + + assert.strictEqual(requests.length, 1) + assert.strictEqual(requests[0].method, "POST") + assert.strictEqual(requests[0].url, "https://api.typesafe.ai/v1/systemone") + assert.strictEqual(requests[0].headers["authorization"], "Bearer ts-test-key") + + const body = yield* getRequestBody(requests[0]) + assert.deepStrictEqual(body, { + state: { + message: ticket.message, + orderId: "A-104" + }, + model: "jev-latest", + questions: { + department: { + type: "choice", + instructions: "Which team should handle this", + criteria: { + billing: "Payment or subscription issues", + technical: "Bugs or integration problems", + sales: "Pricing or account questions" + } + }, + frustration: { + type: "score", + instructions: "How frustrated the customer appears", + criteria: ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"] + }, + urgent: { + type: "noul", + instructions: "The message conveys urgency or time-sensitivity", + criteria: { + false: "No time pressure", + true: "Needs action now" + } + } + } + }) + })) + + it.effect("maps choice, score, and noul answers onto classify, rate, and probability answers", () => + Effect.gen(function*() { + const { answers, usage } = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => Effect.succeed(jsonResponse(request, systemOneResponse)))) + ) + + assert.strictEqual(answers.department.label, "billing") + assert.deepStrictEqual(answers.department.probabilities, { billing: 0.84, technical: 0.159, sales: 0.001 }) + assert.strictEqual(answers.department.confidence, 0.596) + + assert.strictEqual(answers.frustration.rating, 1.6) + assert.strictEqual(answers.frustration.label, "Very angry, strong language") + assert.deepStrictEqual(answers.frustration.probabilities, { + "Calm, just stating facts": 0.05, + "Frustrated but civil": 0.3, + "Very angry, strong language": 0.65 + }) + assert.strictEqual(answers.frustration.confidence, 0.78) + + assert.strictEqual(answers.urgent.probability, 0.999) + assert.isFalse("confidence" in answers.urgent) + + assert.strictEqual(usage.inputTokens, 312) + assert.strictEqual(usage.outputTokens, 48) + })) + + it.effect("encodes an explicitly undefined optional input field as JSON null", () => + Effect.gen(function*() { + const requests: Array = [] + const definition = Decision.make({ + input: Schema.Struct({ message: Schema.String, orderId: Schema.optional(Schema.String) }), + decisions: TicketTriage.decisions + }) + const { answers } = yield* DecisionModel.decide(definition, { + input: { message: ticket.message, orderId: undefined } + }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, systemOneResponse)) + })) + ) + + assert.strictEqual(requests.length, 1) + const body = yield* getRequestBody(requests[0]) + assert.deepStrictEqual(body.state, { message: ticket.message, orderId: null }) + assert.strictEqual(answers.urgent.probability, 0.999) + })) + + for ( + const { extra, inputTokens, name, outputTokens } of [ + { name: "absent usage", extra: {}, inputTokens: undefined, outputTokens: undefined }, + { name: "empty usage", extra: { usage: {} }, inputTokens: undefined, outputTokens: undefined }, + { name: "input tokens only", extra: { usage: { input_tokens: 312 } }, inputTokens: 312, outputTokens: undefined }, + { name: "output tokens only", extra: { usage: { output_tokens: 48 } }, inputTokens: undefined, outputTokens: 48 } + ] + ) { + it.effect("preserves answers with " + name, () => + Effect.gen(function*() { + const { usage: _usage, ...response } = systemOneResponse + const { answers, usage } = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => Effect.succeed(jsonResponse(request, { ...response, ...extra })))) + ) + + assert.strictEqual(answers.department.label, "billing") + assert.strictEqual(answers.frustration.rating, 1.6) + assert.strictEqual(answers.urgent.probability, 0.999) + assert.strictEqual(usage.inputTokens, inputTokens) + assert.strictEqual(usage.outputTokens, outputTokens) + })) + } + + it.effect("sends a pinned model id through unchanged", () => + Effect.gen(function*() { + const requests: Array = [] + + const modelName = yield* Effect.gen(function*() { + yield* DecisionModel.decide(TicketTriage, { input: ticket }) + return yield* Model.ModelName + }).pipe( + Effect.provide(TypeSafeDecisionModel.model("jev-1.13.0")), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, { ...systemOneResponse, model: "jev-1.13.0" })) + })) + ) + + const body = yield* getRequestBody(requests[0]) + assert.strictEqual(modelName, "jev-1.13.0") + assert.strictEqual(body.model, "jev-1.13.0") + })) + + it.effect("passes a string input through as a string state", () => + Effect.gen(function*() { + const requests: Array = [] + const Sentiment = Decision.make({ + input: Schema.String, + decisions: { + urgent: Decision.probability({ + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + }) + } + }) + + yield* DecisionModel.decide(Sentiment, { input: "My card was charged twice." }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, { + model: "jev-latest", + answers: { urgent: { type: "noul", noul: 0.93 } }, + usage: { input_tokens: 20, output_tokens: 4 } + })) + })) + ) + + const body = yield* getRequestBody(requests[0]) + assert.strictEqual(body.state, "My card was charged twice.") + })) + + it.effect("passes an array input through as an array state", () => + Effect.gen(function*() { + const requests: Array = [] + const Conversation = Decision.make({ + input: Schema.Array(Schema.String), + decisions: { + urgent: Decision.probability({ + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + }) + } + }) + + yield* DecisionModel.decide(Conversation, { + input: ["Hi", "My customer number is TS1337.", "My card was charged twice."] + }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => { + requests.push(request) + return Effect.succeed(jsonResponse(request, { + model: "jev-latest", + answers: { urgent: { type: "noul", noul: 0.93 } }, + usage: { input_tokens: 20, output_tokens: 4 } + })) + })) + ) + + const body = yield* getRequestBody(requests[0]) + assert.deepStrictEqual(body.state, ["Hi", "My customer number is TS1337.", "My card was charged twice."]) + })) + + it.effect("fails with InvalidOutputError when an answer type does not match the decision", () => + Effect.gen(function*() { + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...systemOneResponse, + answers: { + ...systemOneResponse.answers, + urgent: { + type: "choice", + choice: "true", + probabilities: { true: 0.9, false: 0.1 }, + confidence: 0.8 + } + } + })) + )), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when an answer is missing", () => + Effect.gen(function*() { + const { urgent: _urgent, ...answers } = systemOneResponse.answers + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide( + makeClientLayer((request) => Effect.succeed(jsonResponse(request, { ...systemOneResponse, answers }))) + ), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when a score answer omits probabilities", () => + Effect.gen(function*() { + const { probabilities: _probabilities, ...frustration } = systemOneResponse.answers.frustration + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...systemOneResponse, + answers: { ...systemOneResponse.answers, frustration } + })) + )), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when a score distribution skips a level index", () => + Effect.gen(function*() { + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...systemOneResponse, + answers: { + ...systemOneResponse.answers, + frustration: { + ...systemOneResponse.answers.frustration, + probabilities: { "0": 0.35, "2": 0.65 } + } + } + })) + )), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("surfaces client errors through decide", () => + Effect.gen(function*() { + const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + Effect.provide(TypeSafeDecisionModel.layer({ model: "jev-latest" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify({ message: "Rate limit exceeded" }), { + status: 429, + headers: { "content-type": "application/json", "retry-after": "3" } + }) + ) + ) + )), + Effect.flip + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "RateLimitError") + })) +}) + +const makeClientLayer = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + TypeSafeClient.layer({ apiKey: Redacted.make("ts-test-key") }).pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, makeHttpClient(handler))) + ) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return yield* handler(request) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const jsonResponse = ( + request: HttpClientRequest.HttpClientRequest, + body: unknown +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + status: 200, + headers: { + "content-type": "application/json" + } + }) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) + +const noopTypeSafeClient: TypeSafeClient.Service = { + client: undefined as unknown as TypeSafeClient.Service["client"], + systemOne: () => Effect.die(new Error("noop")), + listModels: () => Effect.die(new Error("noop")) +} diff --git a/repos/effect/packages/ai/typesafe/test/TypeSafeSchema.test.ts b/repos/effect/packages/ai/typesafe/test/TypeSafeSchema.test.ts new file mode 100644 index 000000000..4de6ac02e --- /dev/null +++ b/repos/effect/packages/ai/typesafe/test/TypeSafeSchema.test.ts @@ -0,0 +1,134 @@ +import * as TypeSafeSchema from "@effect/ai-typesafe/TypeSafeSchema" +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" + +const choiceAnswer = { + type: "choice", + choice: "billing", + probabilities: { billing: 0.84, technical: 0.159, sales: 0.001 }, + confidence: 0.596 +} + +const scoreAnswer = { + type: "score", + score: 1.6, + legend: { "0": "Calm", "1": "Frustrated", "2": "Very angry" }, + probabilities: { "0": 0.05, "1": 0.3, "2": 0.65 }, + confidence: 0.78 +} + +const noulAnswer = { + type: "noul", + noul: 0.999 +} + +describe("TypeSafeSchema", () => { + it("encodes a systemone request with every question type", () => { + const request = { + state: { message: "My card was charged twice.", order_id: "A-104" }, + model: "jev-latest", + questions: { + department: { + type: "choice", + instructions: "Which team should handle this", + criteria: { billing: "Payment or subscription issues", technical: "Bugs or integration problems" } + }, + frustration: { + type: "score", + instructions: "How frustrated the customer appears", + criteria: ["Calm", "Frustrated", "Very angry"] + }, + urgent: { + type: "noul", + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + } + } + } as const + + const decoded = Schema.decodeUnknownSync(TypeSafeSchema.SystemOneRequest)(request) + const encoded = Schema.encodeSync(TypeSafeSchema.SystemOneRequest)(decoded) + + assert.deepStrictEqual(JSON.parse(JSON.stringify(encoded)), request) + }) + + it("accepts a noul question without criteria", () => { + const decoded = Schema.decodeUnknownSync(TypeSafeSchema.NoulQuestion)({ + type: "noul", + instructions: "The message conveys urgency" + }) + + assert.strictEqual(decoded.type, "noul") + assert.isUndefined(decoded.criteria) + }) + + it("decodes a systemone response with choice, score, and noul answers", () => { + const response = { + model: "jev-latest", + answers: { + department: choiceAnswer, + frustration: scoreAnswer, + urgent: noulAnswer + }, + usage: { input_tokens: 312, output_tokens: 48 } + } + + const decoded = Schema.decodeUnknownSync(TypeSafeSchema.SystemOneResponse)(response) + + assert.deepStrictEqual(JSON.parse(JSON.stringify(decoded)), response) + }) + + it("decodes a score answer without a legend", () => { + const { legend: _legend, ...withoutLegend } = scoreAnswer + const decoded = Schema.decodeUnknownSync(TypeSafeSchema.ScoreAnswer)(withoutLegend) + + assert.strictEqual(decoded.score, 1.6) + assert.deepStrictEqual(decoded.probabilities, { "0": 0.05, "1": 0.3, "2": 0.65 }) + }) + + it.each([ + { name: "score answer without probabilities", answer: { type: "score", score: 1.6, confidence: 0.78 } }, + { name: "score answer without score", answer: { type: "score", probabilities: { "0": 1 }, confidence: 1 } }, + { name: "choice answer without probabilities", answer: { type: "choice", choice: "billing", confidence: 1 } }, + { name: "choice answer without choice", answer: { type: "choice", probabilities: { billing: 1 }, confidence: 1 } }, + { name: "noul answer without noul", answer: { type: "noul" } }, + { name: "answer with an unknown type", answer: { type: "rank", rank: [] } } + ])("rejects a $name", ({ answer }) => { + assert.throws(() => Schema.decodeUnknownSync(TypeSafeSchema.Answer)(answer)) + }) + + it.each([ + { name: "absent usage", extra: {} }, + { name: "empty usage", extra: { usage: {} } }, + { name: "input tokens only", extra: { usage: { input_tokens: 312 } } }, + { name: "output tokens only", extra: { usage: { output_tokens: 48 } } } + ])("decodes a response with $name", ({ extra }) => { + const response = { model: "jev-latest", answers: { urgent: noulAnswer }, ...extra } + const decoded = Schema.decodeUnknownSync(TypeSafeSchema.SystemOneResponse)(response) + + assert.deepStrictEqual(JSON.parse(JSON.stringify(decoded)), response) + }) + + it.each([ + { name: "neither optional field", model: { name: "jev-latest" } }, + { name: "description only", model: { name: "jev-latest", description: "Latest Jev" } }, + { name: "release date only", model: { name: "jev-latest", release_date: "2026-08-01" } } + ])("decodes a model with $name", ({ model }) => { + const response = { models: [model] } + const decoded = Schema.decodeUnknownSync(TypeSafeSchema.ListModelsResponse)(response) + + assert.deepStrictEqual(JSON.parse(JSON.stringify(decoded)), response) + }) + + it("decodes a models list response", () => { + const response = { + models: [ + { name: "jev-1.13.0", description: "Jev 1.13", release_date: "2026-08-01" } + ] + } + + const decoded = Schema.decodeUnknownSync(TypeSafeSchema.ListModelsResponse)(response) + + assert.deepStrictEqual(JSON.parse(JSON.stringify(decoded)), response) + }) +}) diff --git a/repos/effect/packages/ai/typesafe/tsconfig.json b/repos/effect/packages/ai/typesafe/tsconfig.json new file mode 100644 index 000000000..e2a8ca19a --- /dev/null +++ b/repos/effect/packages/ai/typesafe/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ] +} diff --git a/repos/effect/packages/ai/typesafe/typetest/TypeSafeDecisionModel.tst.ts b/repos/effect/packages/ai/typesafe/typetest/TypeSafeDecisionModel.tst.ts new file mode 100644 index 000000000..0c963a8f2 --- /dev/null +++ b/repos/effect/packages/ai/typesafe/typetest/TypeSafeDecisionModel.tst.ts @@ -0,0 +1,78 @@ +import { TypeSafeClient, TypeSafeDecisionModel } from "@effect/ai-typesafe" +import { type Config, Effect, type Layer, Redacted, Schema } from "effect" +import { Decision, DecisionModel, type Model } from "effect/unstable/ai" +import type { HttpClient } from "effect/unstable/http" +import { describe, expect, it } from "tstyche" + +declare const acceptsKnownModel: (model: TypeSafeDecisionModel.Model) => void + +const Triage = Decision.make({ + input: Schema.String, + decisions: { + urgent: Decision.probability({ + instructions: "The message conveys urgency", + criteria: { false: "No time pressure", true: "Needs action now" } + }) + } +}) + +describe("TypeSafeDecisionModel", () => { + describe("Model", () => { + it("keeps the known model ids as literals, while the constructors still accept custom ids", () => { + expect(acceptsKnownModel).type.toBeCallableWith("jev-latest") + expect(acceptsKnownModel).type.toBeCallableWith("jev-preview") + expect(acceptsKnownModel).type.toBeCallableWith("jev-1.13.0") + expect(acceptsKnownModel).type.not.toBeCallableWith("not-a-real-model") + expect(TypeSafeDecisionModel.model).type.toBeCallableWith("not-a-real-model") + expect(TypeSafeDecisionModel.layer).type.toBeCallableWith({ model: "not-a-real-model" }) + }) + }) + + describe("model", () => { + it("provides DecisionModel and requires TypeSafeClient", () => { + expect(TypeSafeDecisionModel.model("jev-latest")).type.toBe< + Model.Model<"typesafe", DecisionModel.DecisionModel, TypeSafeClient.TypeSafeClient> + >() + }) + + it("satisfies a decide program's DecisionModel requirement", () => { + const program = DecisionModel.decide(Triage, { input: "My card was charged twice." }).pipe( + Effect.provide(TypeSafeDecisionModel.model("jev-latest")) + ) + + type Requirements = typeof program extends Effect.Effect ? R : never + + expect().type.toBe() + }) + }) + + describe("layer", () => { + it("provides DecisionModel and requires TypeSafeClient", () => { + expect(TypeSafeDecisionModel.layer({ model: "jev-latest" })).type.toBe< + Layer.Layer + >() + }) + }) + + describe("make", () => { + it("builds a DecisionModel from the TypeSafeClient service", () => { + expect(TypeSafeDecisionModel.make({ model: "jev-latest" })).type.toBe< + Effect.Effect + >() + }) + }) +}) + +describe("TypeSafeClient", () => { + it("layer requires only HttpClient", () => { + expect(TypeSafeClient.layer({ apiKey: Redacted.make("ts-test-key") })).type.toBe< + Layer.Layer + >() + }) + + it("layerConfig fails with ConfigError", () => { + expect(TypeSafeClient.layerConfig()).type.toBe< + Layer.Layer + >() + }) +}) diff --git a/repos/effect/packages/atom/react/CHANGELOG.md b/repos/effect/packages/atom/react/CHANGELOG.md index ba66b0a15..68cbcf6a3 100644 --- a/repos/effect/packages/atom/react/CHANGELOG.md +++ b/repos/effect/packages/atom/react/CHANGELOG.md @@ -1,5 +1,41 @@ # @effect/atom-react +## 4.0.0-rc.117 + +### Patch Changes + +- Updated dependencies [[`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9), [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724), [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0), [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b), [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9), [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a), [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5), [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741)]: + - effect@4.0.0-rc.117 + +## 4.0.0-rc.116 + +### Patch Changes + +- Updated dependencies [[`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f), [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2), [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d), [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356), [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf), [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b), [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f), [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a), [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac), [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06), [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8), [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153), [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537), [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746), [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9), [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c), [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3), [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b), [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f), [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5), [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55), [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b)]: + - effect@4.0.0-rc.116 + +## 4.0.0-rc.115 + +### Patch Changes + +- Updated dependencies [[`657254b`](https://github.com/Effect-TS/effect/commit/657254b8218628b0116497d09aaf783cb88279f3), [`f9ef0e9`](https://github.com/Effect-TS/effect/commit/f9ef0e970260b5de143392d00593e6c8b4614cff), [`4f73f9e`](https://github.com/Effect-TS/effect/commit/4f73f9e0bd9538260f0f7166e0e6abdbd507a0e8)]: + - effect@4.0.0-rc.115 + +## 4.0.0-rc.114 + +### Patch Changes + +- Updated dependencies [[`3ff4952`](https://github.com/Effect-TS/effect/commit/3ff49521afa6fe6c85c2c230f7e986f7374ef353), [`6d55555`](https://github.com/Effect-TS/effect/commit/6d55555b83701c42855758944985a6fdbf57fffe), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`d4e4ad5`](https://github.com/Effect-TS/effect/commit/d4e4ad57d06d0a30d5e8090ed951f080bcd3a3d7), [`b1988f4`](https://github.com/Effect-TS/effect/commit/b1988f496eef07482f10f4cc03ef20b9570ea0ac), [`482b7d7`](https://github.com/Effect-TS/effect/commit/482b7d7eb08ebe6bc57781414a5a9a12600b2c71), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`9941e6d`](https://github.com/Effect-TS/effect/commit/9941e6dbf800cfd3723bfb421306e04e36b5882d)]: + - effect@4.0.0-rc.114 + +## 4.0.0-rc.113 + +### Patch Changes + +- [#7744](https://github.com/Effect-TS/effect/pull/7744) [`474860b`](https://github.com/Effect-TS/effect/commit/474860b4629ca2d01f7bff8f310bce3f3f35c42c) Thanks @kitlangton! - Ensure `useAtomRef` updates after switching refs when the new value matches the previous ref's value. +- Updated dependencies [[`49e3901`](https://github.com/Effect-TS/effect/commit/49e39010ff84acbde32c50d559a2f76426cf830e), [`b945ded`](https://github.com/Effect-TS/effect/commit/b945ded23aa9a0ad88bb55aa4089680866dccf92), [`d6422f4`](https://github.com/Effect-TS/effect/commit/d6422f4104062e3e176abe92ef0e444cd4cc6bab), [`5a80204`](https://github.com/Effect-TS/effect/commit/5a802043984727b0c5a291af39d1b9bbfa8d7b8b), [`53511ef`](https://github.com/Effect-TS/effect/commit/53511efcd1c7a05d34b4c9a19c19ed317ebc5d4c), [`79ae49f`](https://github.com/Effect-TS/effect/commit/79ae49f19cab9a42c8f27ae8b104d5e39e6131fa), [`0d083ba`](https://github.com/Effect-TS/effect/commit/0d083ba26b2e1afec8d3e8d83db0d05683b6602b), [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803), [`debe8fd`](https://github.com/Effect-TS/effect/commit/debe8fdc15d00d51cf707353365d7b877123eec6), [`a8588f9`](https://github.com/Effect-TS/effect/commit/a8588f99a43dc9b85dacb14f7be5d3e1ed994d4f), [`f17eb0a`](https://github.com/Effect-TS/effect/commit/f17eb0a6f669e09647c5ded5832ea6baac596659), [`f30cbfe`](https://github.com/Effect-TS/effect/commit/f30cbfe53951128ad8124ae733f12423dbc78bd8), [`78cc9c0`](https://github.com/Effect-TS/effect/commit/78cc9c0d0f36b59f7034b2ace56a4a5c02b7a551), [`ccbdbd5`](https://github.com/Effect-TS/effect/commit/ccbdbd51e7bdb114302701fbc03021868b71956a), [`a63dcbf`](https://github.com/Effect-TS/effect/commit/a63dcbf04e5c3d8d934a41bc6122e9951b1cefa9), [`b845b18`](https://github.com/Effect-TS/effect/commit/b845b186379106c17ed4b1149e2462cd0d2d77b5), [`381b794`](https://github.com/Effect-TS/effect/commit/381b79464f8e0a520585fdcfdd3e95e0553d0b27), [`4ffcaf4`](https://github.com/Effect-TS/effect/commit/4ffcaf4c4a94d3d9d5715db91aa37d7f5d9ecd99), [`ba2fd82`](https://github.com/Effect-TS/effect/commit/ba2fd822e9618f30d0f430ae4eed2eaa4813e2e3), [`02be94c`](https://github.com/Effect-TS/effect/commit/02be94ce78a3c9fc1e4c516d37d8e11e1c0eef2e), [`115d8c2`](https://github.com/Effect-TS/effect/commit/115d8c22599640ece2fd6a10564925b1d79f8a8c), [`1452635`](https://github.com/Effect-TS/effect/commit/14526354f3d0288bd1d0a85c8d4aeee8abe2cd9b), [`77f85fe`](https://github.com/Effect-TS/effect/commit/77f85fe1613348f5c990016b49dc97e252576c82), [`a3f2b31`](https://github.com/Effect-TS/effect/commit/a3f2b319343f28edb028270f7074066cea0ec429), [`310f8d3`](https://github.com/Effect-TS/effect/commit/310f8d3424a025ce1f1cc5c338232db2362052d8), [`291d616`](https://github.com/Effect-TS/effect/commit/291d616282b666bbba3664143fa1d19e839a6a5d), [`48dbbb2`](https://github.com/Effect-TS/effect/commit/48dbbb22bb2e04f63992fc267dd711a0cbd76d72), [`b43bfd6`](https://github.com/Effect-TS/effect/commit/b43bfd644a3205f1518abbfd006f761e31b3a5f9), [`1c89c78`](https://github.com/Effect-TS/effect/commit/1c89c78f9faa3638f398772f3ed7182aa97a1edd), [`9bbe1a5`](https://github.com/Effect-TS/effect/commit/9bbe1a5940eee4642ffe854356128b617e0a064c), [`dd99ab0`](https://github.com/Effect-TS/effect/commit/dd99ab007e3352761187dae330d52f65feeff7c0), [`8f397ed`](https://github.com/Effect-TS/effect/commit/8f397ed4e7852026591819cc4c8c1995bdd2fb8c), [`d7ae6b6`](https://github.com/Effect-TS/effect/commit/d7ae6b6491a88f2710612bfcddaf608ebe925f7c), [`8cf1203`](https://github.com/Effect-TS/effect/commit/8cf12030e8a3e1e76384884d41afa96e2bd72b13), [`87654c5`](https://github.com/Effect-TS/effect/commit/87654c5eed150387f4eac7153751b6085616ba52), [`f05ae0b`](https://github.com/Effect-TS/effect/commit/f05ae0b3fd2cfc7f12b668af244b3cf3fae59bc5), [`436f5eb`](https://github.com/Effect-TS/effect/commit/436f5eb44aa234bdf1de37b07da2ce8dc31ad4ec), [`d8ff960`](https://github.com/Effect-TS/effect/commit/d8ff9605bb5ef7b0cacd80e3b43ed1ce9e3d5e36), [`8766475`](https://github.com/Effect-TS/effect/commit/876647502300b9fb79d163de10fd497716d93be0), [`b64f406`](https://github.com/Effect-TS/effect/commit/b64f406de13a1b9da4664dbde2dbddaa5f776a05), [`4697aaa`](https://github.com/Effect-TS/effect/commit/4697aaa1d38f9da7fe0bcb62f708183f766fe496), [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494), [`9956f0e`](https://github.com/Effect-TS/effect/commit/9956f0e6f47096a6e31305ec2c7320b8e1a140af), [`5c7eed0`](https://github.com/Effect-TS/effect/commit/5c7eed0c5aa0988716206dd374359223edf828ee), [`183c2ea`](https://github.com/Effect-TS/effect/commit/183c2ea65ee1e9b967c746aa1b0bf2b8d3422de2), [`1e92dbd`](https://github.com/Effect-TS/effect/commit/1e92dbd26f5a74bb32103cbaac0f09460b7954de), [`4eb0fa7`](https://github.com/Effect-TS/effect/commit/4eb0fa72302b0a63ac34c6515a9cdf96338e586a), [`534b8b9`](https://github.com/Effect-TS/effect/commit/534b8b9dba195ec38a4795fe564d5e0876cb6468), [`b76a1cf`](https://github.com/Effect-TS/effect/commit/b76a1cf32b0a742c24c1874137e2da8fed5c48eb), [`acc1e53`](https://github.com/Effect-TS/effect/commit/acc1e5384aa5cb7b072f60308013f078b0099bdf), [`4950a91`](https://github.com/Effect-TS/effect/commit/4950a914a68306531a1111d4cdc4504aa8d3854e), [`c020987`](https://github.com/Effect-TS/effect/commit/c020987a0611c9e610a7eef347648b60026745b8), [`abe95d1`](https://github.com/Effect-TS/effect/commit/abe95d12340e1dd435e5df623b1967bf01938c94), [`027ceb9`](https://github.com/Effect-TS/effect/commit/027ceb91e003fbc36f0559f72e1c614cd9198c1a), [`3d203b7`](https://github.com/Effect-TS/effect/commit/3d203b702e26687d2fea4aaae7cc01dcfb82c05e), [`a29b8f4`](https://github.com/Effect-TS/effect/commit/a29b8f4de5aee58dd9ee645365c04cfe1ea6fa9f), [`ce4aa65`](https://github.com/Effect-TS/effect/commit/ce4aa65409e8500908fb6496eca06270ef5b636f), [`a8ea807`](https://github.com/Effect-TS/effect/commit/a8ea80799a0bf187c577e052fef0b7245494c7bf), [`a3ebb7f`](https://github.com/Effect-TS/effect/commit/a3ebb7fcb208f3933a40a0fd949dbcfdf1ca2921), [`473bd81`](https://github.com/Effect-TS/effect/commit/473bd814a97dba66070435cfe84933bcc143effe), [`53843f6`](https://github.com/Effect-TS/effect/commit/53843f6490f4eebaf1eeb91fdcc5f1c542b0e132), [`c5eca65`](https://github.com/Effect-TS/effect/commit/c5eca6539f0d605acd5ce19437fa88f167d69bb7), [`d6f9eba`](https://github.com/Effect-TS/effect/commit/d6f9eba7e8df0c8bd74762c313402884d94ba146), [`8d1e97a`](https://github.com/Effect-TS/effect/commit/8d1e97adbf5a36b4b53ab56f797c2ec0267a8821), [`b28ab48`](https://github.com/Effect-TS/effect/commit/b28ab48be7cf7f2bd30f9467ccac0ab41260f7ad), [`9960708`](https://github.com/Effect-TS/effect/commit/9960708052fcaaa873c471aea144ca46955e6023), [`8ac53b6`](https://github.com/Effect-TS/effect/commit/8ac53b6ae677f06d88d7d81f932ab5ff9b59a10e), [`84ad49a`](https://github.com/Effect-TS/effect/commit/84ad49a357ef156b4ff46cefc945efbe3e292eff), [`72cfa24`](https://github.com/Effect-TS/effect/commit/72cfa2420977919ba57a8e6de538e01e06d5f380), [`95c2581`](https://github.com/Effect-TS/effect/commit/95c2581e0f78e1537aad21764fe562206ca50adb), [`d5c7cd2`](https://github.com/Effect-TS/effect/commit/d5c7cd2c47b2b01523aaea48d2dd039b89b36114), [`fe4fed1`](https://github.com/Effect-TS/effect/commit/fe4fed1b014c78043a8efe37520c527100910010), [`d150a64`](https://github.com/Effect-TS/effect/commit/d150a643c4f60da2dc19ae813b807d48b580087e), [`05b1e80`](https://github.com/Effect-TS/effect/commit/05b1e80cde3a8da1ff1afe288f8539b72c05bb87), [`414dc90`](https://github.com/Effect-TS/effect/commit/414dc9056a39aa22f7cc27aeb5e631231ce50982), [`3f51acd`](https://github.com/Effect-TS/effect/commit/3f51acdf5e2490d7d0a833cd67ce06d1d867facd), [`d68ff05`](https://github.com/Effect-TS/effect/commit/d68ff05506ab5bb3ec07e1b759ce296423b0dbbe), [`f3cf1e6`](https://github.com/Effect-TS/effect/commit/f3cf1e6c36f1aac8399d9b540aecb6410be3096f), [`6525771`](https://github.com/Effect-TS/effect/commit/6525771eef7df6b71a3ef6e299ee93d798ed3dda), [`4ab4e83`](https://github.com/Effect-TS/effect/commit/4ab4e838a4c9ab3c9cc0d52d3e83de88686601b0), [`f7f1d78`](https://github.com/Effect-TS/effect/commit/f7f1d78e52023bdfbebf76949c550db8bb9411dd), [`47b358a`](https://github.com/Effect-TS/effect/commit/47b358a4b85d22973a7957abd1a859f65b75b7da), [`45ffa72`](https://github.com/Effect-TS/effect/commit/45ffa7221259c7e4be9797b90f6ca569fe7ec8a3), [`6232650`](https://github.com/Effect-TS/effect/commit/62326505a74fcd8fe27c8e2064128d2dea484ecc), [`4b73e1b`](https://github.com/Effect-TS/effect/commit/4b73e1bd5576346d7939be8915f3053e428d1c0e), [`284050c`](https://github.com/Effect-TS/effect/commit/284050c2aa6e78eceb08e5eff1803501cc80a658), [`c85fc0b`](https://github.com/Effect-TS/effect/commit/c85fc0b8f2195780929d6fc8ecdc084faf18736c), [`7999b07`](https://github.com/Effect-TS/effect/commit/7999b078950fda15e235db1e61e8db5eb0632528), [`7d455f5`](https://github.com/Effect-TS/effect/commit/7d455f51acf2f0e9f710f298e14e4d81179a9259), [`d681c2e`](https://github.com/Effect-TS/effect/commit/d681c2eb1bb07d782540286368e8b42533df3063), [`84d2a47`](https://github.com/Effect-TS/effect/commit/84d2a470ecc0fcad7f1d1e7a50464e02af0e6972), [`ed74b18`](https://github.com/Effect-TS/effect/commit/ed74b18b95b0797808f2f7928f413f1cf2279d77), [`2245997`](https://github.com/Effect-TS/effect/commit/2245997ad2214591b7d9674777595092926743cf), [`d386979`](https://github.com/Effect-TS/effect/commit/d386979fedbdd433d28816f9cc74675a9af45189), [`fc9fedf`](https://github.com/Effect-TS/effect/commit/fc9fedf59fc26a8bc6e45c3860a921f3b7e0f971), [`7750dbe`](https://github.com/Effect-TS/effect/commit/7750dbe0d5b9929c6d92b0365b42784b993ea2a6), [`fc91af6`](https://github.com/Effect-TS/effect/commit/fc91af659d0682659e6347eede288f90f618acc0), [`39b9738`](https://github.com/Effect-TS/effect/commit/39b9738727ab4d87298fd640edd138f352c70ee3), [`88093b5`](https://github.com/Effect-TS/effect/commit/88093b57951a1b597599fe4772ab690b36a95818), [`d14c463`](https://github.com/Effect-TS/effect/commit/d14c4631ac82911a8ca0b6c24c14ecb95863ee6b), [`d473bd3`](https://github.com/Effect-TS/effect/commit/d473bd3ee966a3a0c245d0e3c752fd91368b8ff6), [`84864bc`](https://github.com/Effect-TS/effect/commit/84864bc30c9e92a1226f65bb78b0641a7e0acea2), [`e2ae724`](https://github.com/Effect-TS/effect/commit/e2ae72481e755a454f28ce45e90f43b857b7c6e5), [`f921ed3`](https://github.com/Effect-TS/effect/commit/f921ed305c63415dc468e94d9319cc3237119138), [`1df933d`](https://github.com/Effect-TS/effect/commit/1df933d1dee2e09a25f9c27214b31aa94fa9d020), [`829aff9`](https://github.com/Effect-TS/effect/commit/829aff97d71d7900b7765ba2f63e33874b898cdc), [`aa0aba3`](https://github.com/Effect-TS/effect/commit/aa0aba383382ca1b99fbe618dceba63c3a5a6a53), [`a71140f`](https://github.com/Effect-TS/effect/commit/a71140fbe79809aa5311a55b1c6908905dc2df26), [`0276a27`](https://github.com/Effect-TS/effect/commit/0276a27ddbb15d163daf56f059ca9f62c4fd9939), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`c86c999`](https://github.com/Effect-TS/effect/commit/c86c99939e1628813a2f92f0906cfbbe924504e8), [`975f758`](https://github.com/Effect-TS/effect/commit/975f7586f3dcda5743d6570a740013a9fbdce9a7), [`2a3a478`](https://github.com/Effect-TS/effect/commit/2a3a4783bcd2d4e91e3505c7a6c1e398fea89595), [`1e6e206`](https://github.com/Effect-TS/effect/commit/1e6e206f6327a114f55a541775f465c0bc9fbf3e), [`fa6027b`](https://github.com/Effect-TS/effect/commit/fa6027b16e65376078e526fa33116ac0cc82485b), [`7616f73`](https://github.com/Effect-TS/effect/commit/7616f73c16bd45a3a3e2202d3f81c6d1cc63f4e0), [`fc668b6`](https://github.com/Effect-TS/effect/commit/fc668b61e9755545a47582dee6c4425d5652cbb6), [`d425c8c`](https://github.com/Effect-TS/effect/commit/d425c8c1319519e861714bcb9d055b63a9434fad), [`ce120f4`](https://github.com/Effect-TS/effect/commit/ce120f474b87d8a5a787dd7f848177ecd3b960fc), [`248201f`](https://github.com/Effect-TS/effect/commit/248201f7846d855685a4dc1b2f01968981706d69), [`e80d397`](https://github.com/Effect-TS/effect/commit/e80d3972fcca4e986b6497d67406f45ee1be540d), [`f1a941d`](https://github.com/Effect-TS/effect/commit/f1a941d52c2f71973e998883869e4e9ef510abf2), [`73bc3a1`](https://github.com/Effect-TS/effect/commit/73bc3a13b6e1b7a060e6bcd19ae7796443211a80), [`b628bb1`](https://github.com/Effect-TS/effect/commit/b628bb1240a09df2244c3ff6c587f978feb25225), [`6e3ae7b`](https://github.com/Effect-TS/effect/commit/6e3ae7b6359bda08cea74b761fd54224d7af45e2), [`e891247`](https://github.com/Effect-TS/effect/commit/e8912473bd4128714a0f8cb04621f3e12f62a910), [`53e6c73`](https://github.com/Effect-TS/effect/commit/53e6c7311b948a62e93e57f6daee66a8ec24b030), [`1579d6f`](https://github.com/Effect-TS/effect/commit/1579d6fd063e444e4fcdfd08ff194c7485203f9c), [`d3c6b73`](https://github.com/Effect-TS/effect/commit/d3c6b73cc4b78f22fdb2e25c8bebda027551540b), [`4a59c6a`](https://github.com/Effect-TS/effect/commit/4a59c6a559e69d399b00c6acd48c8e1977e34875), [`145d8e1`](https://github.com/Effect-TS/effect/commit/145d8e1013220425b8edf34f7011c73f73e1cdcf), [`f74282c`](https://github.com/Effect-TS/effect/commit/f74282cb08de808b678ce67687acbcd61939d2f6), [`0a38623`](https://github.com/Effect-TS/effect/commit/0a3862349ebfbc41746aa0de5e682992719c1da6), [`0a08ae0`](https://github.com/Effect-TS/effect/commit/0a08ae0626f8124779ec2e32ae2088d28db157c6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`d60c5d4`](https://github.com/Effect-TS/effect/commit/d60c5d40b5954ea1557aef1502b4b87f98bbf134), [`07ffd25`](https://github.com/Effect-TS/effect/commit/07ffd25bd0e82c9e6b72b8e669833fb4531a9da3), [`9b517ad`](https://github.com/Effect-TS/effect/commit/9b517ad28a0c2f213693bcab408658c40e3b6d9b), [`604b1c1`](https://github.com/Effect-TS/effect/commit/604b1c109d6c9e654b9b7fcc98d6b80361400325), [`ccc2e02`](https://github.com/Effect-TS/effect/commit/ccc2e02def5965f5535ca97fac2051f4387c4e13), [`14d810a`](https://github.com/Effect-TS/effect/commit/14d810a726caad8a7b0d42f04a6876f25fbcc005), [`a9d1ee3`](https://github.com/Effect-TS/effect/commit/a9d1ee3d4d51e97ea33440fe6100935d5fd5aada), [`a31adbe`](https://github.com/Effect-TS/effect/commit/a31adbefd6c04c70536de11433edba9edd1f5013), [`7245f87`](https://github.com/Effect-TS/effect/commit/7245f87ace09806d15893df8f0bac8f0b9b817db), [`3a0828b`](https://github.com/Effect-TS/effect/commit/3a0828b058abb4b4646bd880458b4e636d77d1c3), [`cd83544`](https://github.com/Effect-TS/effect/commit/cd83544dd594b99e8ea0d7a7645c086f5861769b), [`6550a07`](https://github.com/Effect-TS/effect/commit/6550a074c5c501d8a95dcd354f17e9785a1ae1eb), [`6f090d4`](https://github.com/Effect-TS/effect/commit/6f090d4c2032cc35f7f9a9eac900d0329b8ac07e), [`b505c0d`](https://github.com/Effect-TS/effect/commit/b505c0db03e86eb126237690a5867f457eb4237f), [`186dd49`](https://github.com/Effect-TS/effect/commit/186dd4914084fa346a870ce9c637b9c2a6cc8100), [`9f37e58`](https://github.com/Effect-TS/effect/commit/9f37e58d00e06150d96af48fffe780947ca0d519), [`4446451`](https://github.com/Effect-TS/effect/commit/44464510114773e1ebd1abb6efffa382cf69998e), [`58be972`](https://github.com/Effect-TS/effect/commit/58be97238ee1f6cd26f6a3a645d1df97a091a224), [`1320075`](https://github.com/Effect-TS/effect/commit/1320075fd4ed381f60f6f37f280dbec17cb29b81), [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80), [`f7490d4`](https://github.com/Effect-TS/effect/commit/f7490d49c75b99118c6c42549511ffa166459f59), [`c8ea602`](https://github.com/Effect-TS/effect/commit/c8ea60231ad69e23c0bf319f6f6b3ff28fc45e4b), [`62d82f4`](https://github.com/Effect-TS/effect/commit/62d82f4919c72d8cc0cfd2b704ed6900f69ad9a8), [`a2c9e7c`](https://github.com/Effect-TS/effect/commit/a2c9e7c17a4118b74a1eab3332042dafe0bb7318), [`97dd022`](https://github.com/Effect-TS/effect/commit/97dd022fe7fbd1696dd60f3319eae594abdd3760), [`f984ee8`](https://github.com/Effect-TS/effect/commit/f984ee82d255c7958eeed6f0dd08de1b88cbbfd6), [`f1b2910`](https://github.com/Effect-TS/effect/commit/f1b291027c3055f801c2f288dda21ba84c361956), [`a29e05a`](https://github.com/Effect-TS/effect/commit/a29e05a907053a8617763b4e7408e1f012b59049), [`1aa1d8b`](https://github.com/Effect-TS/effect/commit/1aa1d8bd0f2c2d429938657927f34f8ff0f940a0), [`bb99734`](https://github.com/Effect-TS/effect/commit/bb99734ef87bcc377c628bc4072aa856ce5f7ca6), [`222e7ca`](https://github.com/Effect-TS/effect/commit/222e7caa4bb0da6d81a4eabf25dffc181c20f6a3), [`797c9e3`](https://github.com/Effect-TS/effect/commit/797c9e3c1f2e66db1570cf6aa5c4a15e94507bf0), [`b4d5398`](https://github.com/Effect-TS/effect/commit/b4d5398598c04a84054a55873c943d587880058d), [`c8349ed`](https://github.com/Effect-TS/effect/commit/c8349ede1a2add807e9ec9c37612b3a39bf284f8), [`8426e5f`](https://github.com/Effect-TS/effect/commit/8426e5f7ad11f15e39565cf8ba0874bf974f5085), [`26e0085`](https://github.com/Effect-TS/effect/commit/26e0085d098f6e526c085c357b8d82ee068b6518), [`1a86166`](https://github.com/Effect-TS/effect/commit/1a86166f102b517ee4d89dca0095e28f738eee10), [`0856631`](https://github.com/Effect-TS/effect/commit/0856631a89d8f72d206c366905ddac75468a8e85), [`0af0985`](https://github.com/Effect-TS/effect/commit/0af0985d5c132b33e14a2df59e0567d7ba2a1710), [`bc582c9`](https://github.com/Effect-TS/effect/commit/bc582c94313cbb89d87b3844ffffabebbb4ffa83), [`2c63f1e`](https://github.com/Effect-TS/effect/commit/2c63f1e07e375b7958e14929c6a4b358c819a9d0), [`0d98213`](https://github.com/Effect-TS/effect/commit/0d9821346453d44696258103643d5482e63638b4), [`ca6f0dc`](https://github.com/Effect-TS/effect/commit/ca6f0dc4fcea564b351bf505df015d16a8b965ed), [`d8cc9ed`](https://github.com/Effect-TS/effect/commit/d8cc9ed80e1df9c056a46ba62df01f7fc0f531c3), [`42fd969`](https://github.com/Effect-TS/effect/commit/42fd969b45025eed6d60cb87b97c5ff88d187671), [`5641ad3`](https://github.com/Effect-TS/effect/commit/5641ad333a88bb9e56baf886006682678391be0a), [`629870d`](https://github.com/Effect-TS/effect/commit/629870d461b36d3dacb30c8d203b8d20d4beaba0), [`d592c14`](https://github.com/Effect-TS/effect/commit/d592c1492ad895ea72537e10387b735f3ba0e7de), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`53909a9`](https://github.com/Effect-TS/effect/commit/53909a9bf14b29725a08dcccde02bee8e95a6b14), [`78a4269`](https://github.com/Effect-TS/effect/commit/78a42697f6085c87c09b18e8095bb7541bd65261), [`06c6307`](https://github.com/Effect-TS/effect/commit/06c6307d3d5538bc398c67a44239bc3d52ab1a4d), [`0847c41`](https://github.com/Effect-TS/effect/commit/0847c41e2472a347cd8f77d460d3cbdfecd4adf0), [`5a77084`](https://github.com/Effect-TS/effect/commit/5a77084651bc58617b9988606473cf9ba5ca0032), [`96f99b3`](https://github.com/Effect-TS/effect/commit/96f99b325da32bfa535e826d89d838d0edec0dea), [`7bb8781`](https://github.com/Effect-TS/effect/commit/7bb878176b8ad20cf0e671b570caf72e194f9515), [`4907e9b`](https://github.com/Effect-TS/effect/commit/4907e9b2470894ddaf0cbdec2612b59f04a51dbe), [`2a30248`](https://github.com/Effect-TS/effect/commit/2a30248b6eb739f22403456209bc468f2f4ef26a), [`8364ddd`](https://github.com/Effect-TS/effect/commit/8364ddda233d35ee472cc2356312317a60a148a2), [`ad67d8c`](https://github.com/Effect-TS/effect/commit/ad67d8c5a1b195ff5468a9f2e963c59aa0b3166f), [`ec0c087`](https://github.com/Effect-TS/effect/commit/ec0c0875262fb81c7992f5023776b8fa894c49b6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`a2c1ce6`](https://github.com/Effect-TS/effect/commit/a2c1ce62f04214d98d848f4cfe33d677e1a87d39), [`91e9af0`](https://github.com/Effect-TS/effect/commit/91e9af0d5f138933d3a131ca1cfbc52d3bc55481), [`7bd3f34`](https://github.com/Effect-TS/effect/commit/7bd3f34385a06157b249a351e32ffdcbe1cf2c37), [`ef16581`](https://github.com/Effect-TS/effect/commit/ef165814eae0a199fdca5b3c2839bb4fcdfd5b5d), [`1693a87`](https://github.com/Effect-TS/effect/commit/1693a877324273f5a23fa7d7fc892f4ee12f20c2), [`df3fc47`](https://github.com/Effect-TS/effect/commit/df3fc47609eda7843dae2b557c8fe834299e26b4), [`e11be41`](https://github.com/Effect-TS/effect/commit/e11be41dfc73fa6b5643ab9af7df18d6907927d7), [`2e39e8b`](https://github.com/Effect-TS/effect/commit/2e39e8bafef7d3fee9f3b324b7d50cc064376667), [`11c5ee7`](https://github.com/Effect-TS/effect/commit/11c5ee7202acd3bb789d0a8ce78e6c4523071553), [`1742d2f`](https://github.com/Effect-TS/effect/commit/1742d2f48441cb7b1a8d9aca3a8e6f0acbc35a96), [`8efc70e`](https://github.com/Effect-TS/effect/commit/8efc70ea16bee43fd9adde2871cf82a8131b80a0), [`9642776`](https://github.com/Effect-TS/effect/commit/964277661423f398f777ec2eab4b3ecc0b53f53b), [`6680828`](https://github.com/Effect-TS/effect/commit/668082885f52672e4c7fad4e0fe2bfa0e69703e2), [`c34edcb`](https://github.com/Effect-TS/effect/commit/c34edcb6d7a6151d53e19828c92abf7dd8450c2a), [`7b2c5bd`](https://github.com/Effect-TS/effect/commit/7b2c5bd3c394fa4751fb229394ac9c03e401f441), [`1a2ccee`](https://github.com/Effect-TS/effect/commit/1a2ccee2bbb93514dc66e0a9cdeb52a82172ab0e), [`db995df`](https://github.com/Effect-TS/effect/commit/db995df19b0381b64cc39b50aa28d7bd8f2c1294), [`af0ccdd`](https://github.com/Effect-TS/effect/commit/af0ccdd13aa8917bce7161b8bc257fbe51c6a6a5), [`addeaea`](https://github.com/Effect-TS/effect/commit/addeaea0b0abe0dc24ca1d4d0fd194a0e458be70), [`7704034`](https://github.com/Effect-TS/effect/commit/770403411bcd8befef0552a0e40066abc417f6ea), [`e72b12f`](https://github.com/Effect-TS/effect/commit/e72b12fc305710550bc6dcb978e92de8abff88cd), [`310dd9c`](https://github.com/Effect-TS/effect/commit/310dd9ce9681e97f558bb042adb35f6040ab81e3), [`1c2afc1`](https://github.com/Effect-TS/effect/commit/1c2afc15bbf4ce5a91d24da99bc275b5bd744e77), [`44f44ca`](https://github.com/Effect-TS/effect/commit/44f44cae65249801c082c3a3848016eeeac5eb24), [`f43b9d6`](https://github.com/Effect-TS/effect/commit/f43b9d6d99cb591c0377bb1474121827201627c9), [`56e72b3`](https://github.com/Effect-TS/effect/commit/56e72b38c01bbe6ec921ad151f83b7849444f219), [`50ef80e`](https://github.com/Effect-TS/effect/commit/50ef80e251b85b5ee63aed06fe0244b5433ec2fa), [`fc3b718`](https://github.com/Effect-TS/effect/commit/fc3b7187114a31cbb21a250f55fda601bb3edcf0), [`ee336d8`](https://github.com/Effect-TS/effect/commit/ee336d80b4024b51654e7a2b8d869d244153fcdf), [`d12f922`](https://github.com/Effect-TS/effect/commit/d12f922853eff2310dbe9fc9b31562ad81b38435), [`46d8310`](https://github.com/Effect-TS/effect/commit/46d83101e8408c0428d9a8760e06e23a69ad6070), [`59812fd`](https://github.com/Effect-TS/effect/commit/59812fd221cde16fc77c03d326c6a2cf8fbec6b9), [`f9d0dec`](https://github.com/Effect-TS/effect/commit/f9d0dec12c5f229f05d7a74d532a85c6bbffefbf), [`4372c79`](https://github.com/Effect-TS/effect/commit/4372c79a32fcecc7b6b284d49a215b2c4fef9d77), [`81485ef`](https://github.com/Effect-TS/effect/commit/81485ef0288a3321e75d7d4ae32b7e0e06b6f86b), [`bd393d6`](https://github.com/Effect-TS/effect/commit/bd393d63c19bdd0ab212d95576cec89051c8501c), [`0c95c04`](https://github.com/Effect-TS/effect/commit/0c95c04b41c2d5d09e4564fd997572207fd72595)]: + - effect@4.0.0-rc.113 + ## 4.0.0-rc.112 ### Patch Changes diff --git a/repos/effect/packages/atom/react/package.json b/repos/effect/packages/atom/react/package.json index 6c6f494dc..bcea696d4 100644 --- a/repos/effect/packages/atom/react/package.json +++ b/repos/effect/packages/atom/react/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-react", - "version": "4.0.0-rc.112", + "version": "4.0.0-rc.117", "type": "module", "license": "MIT", "description": "React bindings for the Effect Atom modules", @@ -65,17 +65,17 @@ "scheduler": ">=0.25.0 <0.28.0" }, "devDependencies": { - "@testing-library/dom": "^10.4.1", + "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.5", - "@types/scheduler": "^0.26.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@types/scheduler": "^0.28.0", "effect": "workspace:^", - "jsdom": "^30.0.1", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "react-error-boundary": "^6.1.3", - "scheduler": "^0.27.0" + "jsdom": "~30.0.1", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "react-error-boundary": "^6.1.5", + "scheduler": "^0.28.0" } } diff --git a/repos/effect/packages/atom/react/src/Hooks.ts b/repos/effect/packages/atom/react/src/Hooks.ts index 4632ea333..5fe03b33b 100644 --- a/repos/effect/packages/atom/react/src/Hooks.ts +++ b/repos/effect/packages/atom/react/src/Hooks.ts @@ -435,8 +435,8 @@ export const useAtomSubscribe = ( * @since 4.0.0 */ export const useAtomRef = (ref: AtomRef.ReadonlyRef): A => { - const [, setValue] = React.useState(ref.value) - React.useEffect(() => ref.subscribe(setValue), [ref]) + const [, forceUpdate] = React.useReducer((n) => n + 1, 0) + React.useEffect(() => ref.subscribe(forceUpdate), [ref]) return ref.value } diff --git a/repos/effect/packages/atom/react/test/index.test.tsx b/repos/effect/packages/atom/react/test/index.test.tsx index 06c2668b9..f00e71a6f 100644 --- a/repos/effect/packages/atom/react/test/index.test.tsx +++ b/repos/effect/packages/atom/react/test/index.test.tsx @@ -4,6 +4,7 @@ import { Cause, Context, Effect, Latch, Layer } from "effect" import * as Schema from "effect/Schema" import * as AsyncResult from "effect/unstable/reactivity/AsyncResult" import * as Atom from "effect/unstable/reactivity/Atom" +import * as AtomRef from "effect/unstable/reactivity/AtomRef" import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" import * as Hydration from "effect/unstable/reactivity/Hydration" import * as React from "react" @@ -11,10 +12,18 @@ import { Suspense } from "react" import { renderToString } from "react-dom/server" import { ErrorBoundary } from "react-error-boundary" import { beforeEach, describe, expect, it, test, vi } from "vitest" -import { HydrationBoundary, RegistryContext, RegistryProvider, useAtomSuspense, useAtomValue } from "../src/index.ts" +import { + HydrationBoundary, + RegistryContext, + RegistryProvider, + useAtomRef, + useAtomSuspense, + useAtomValue +} from "../src/index.ts" import * as ScopedAtom from "../src/ScopedAtom.ts" -describe("atom-react", () => { +// Tests share the DOM and registry. +describe("atom-react", { concurrent: false }, () => { let registry: AtomRegistry.AtomRegistry beforeEach(() => { @@ -180,6 +189,24 @@ describe("atom-react", () => { }) }) + test("useAtomRef updates after switching refs", () => { + const first = AtomRef.make(0) + const second = AtomRef.make(1) + + function TestComponent({ source }: { readonly source: AtomRef.ReadonlyRef }) { + return
{useAtomRef(source)}
+ } + + const { rerender } = render() + rerender() + + act(() => { + second.set(0) + }) + + expect(screen.getByTestId("value")).toHaveTextContent("0") + }) + describe("ScopedAtom", () => { test("throws when used outside Provider", () => { const counter = ScopedAtom.make(() => Atom.make(0)) diff --git a/repos/effect/packages/atom/solid/CHANGELOG.md b/repos/effect/packages/atom/solid/CHANGELOG.md index 849829970..c5b287547 100644 --- a/repos/effect/packages/atom/solid/CHANGELOG.md +++ b/repos/effect/packages/atom/solid/CHANGELOG.md @@ -1,5 +1,41 @@ # @effect/atom-solid +## 4.0.0-rc.117 + +### Patch Changes + +- Updated dependencies [[`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9), [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724), [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0), [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b), [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9), [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a), [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5), [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741)]: + - effect@4.0.0-rc.117 + +## 4.0.0-rc.116 + +### Patch Changes + +- Updated dependencies [[`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f), [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2), [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d), [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356), [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf), [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b), [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f), [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a), [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac), [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06), [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8), [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153), [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537), [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746), [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9), [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c), [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3), [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b), [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f), [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5), [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55), [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b)]: + - effect@4.0.0-rc.116 + +## 4.0.0-rc.115 + +### Patch Changes + +- Updated dependencies [[`657254b`](https://github.com/Effect-TS/effect/commit/657254b8218628b0116497d09aaf783cb88279f3), [`f9ef0e9`](https://github.com/Effect-TS/effect/commit/f9ef0e970260b5de143392d00593e6c8b4614cff), [`4f73f9e`](https://github.com/Effect-TS/effect/commit/4f73f9e0bd9538260f0f7166e0e6abdbd507a0e8)]: + - effect@4.0.0-rc.115 + +## 4.0.0-rc.114 + +### Patch Changes + +- Updated dependencies [[`3ff4952`](https://github.com/Effect-TS/effect/commit/3ff49521afa6fe6c85c2c230f7e986f7374ef353), [`6d55555`](https://github.com/Effect-TS/effect/commit/6d55555b83701c42855758944985a6fdbf57fffe), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`d4e4ad5`](https://github.com/Effect-TS/effect/commit/d4e4ad57d06d0a30d5e8090ed951f080bcd3a3d7), [`b1988f4`](https://github.com/Effect-TS/effect/commit/b1988f496eef07482f10f4cc03ef20b9570ea0ac), [`482b7d7`](https://github.com/Effect-TS/effect/commit/482b7d7eb08ebe6bc57781414a5a9a12600b2c71), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`9941e6d`](https://github.com/Effect-TS/effect/commit/9941e6dbf800cfd3723bfb421306e04e36b5882d)]: + - effect@4.0.0-rc.114 + +## 4.0.0-rc.113 + +### Patch Changes + +- [#7533](https://github.com/Effect-TS/effect/pull/7533) [`7e24e3a`](https://github.com/Effect-TS/effect/commit/7e24e3a5ef62fd46f8fbee814c7da8f94d834c75) Thanks @apoorvdarshan! - Allow `RegistryProvider` to leave `defaultIdleTTL` undefined, matching the React binding and enabling immediate cleanup of unused atoms unless a TTL is explicitly configured. +- Updated dependencies [[`49e3901`](https://github.com/Effect-TS/effect/commit/49e39010ff84acbde32c50d559a2f76426cf830e), [`b945ded`](https://github.com/Effect-TS/effect/commit/b945ded23aa9a0ad88bb55aa4089680866dccf92), [`d6422f4`](https://github.com/Effect-TS/effect/commit/d6422f4104062e3e176abe92ef0e444cd4cc6bab), [`5a80204`](https://github.com/Effect-TS/effect/commit/5a802043984727b0c5a291af39d1b9bbfa8d7b8b), [`53511ef`](https://github.com/Effect-TS/effect/commit/53511efcd1c7a05d34b4c9a19c19ed317ebc5d4c), [`79ae49f`](https://github.com/Effect-TS/effect/commit/79ae49f19cab9a42c8f27ae8b104d5e39e6131fa), [`0d083ba`](https://github.com/Effect-TS/effect/commit/0d083ba26b2e1afec8d3e8d83db0d05683b6602b), [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803), [`debe8fd`](https://github.com/Effect-TS/effect/commit/debe8fdc15d00d51cf707353365d7b877123eec6), [`a8588f9`](https://github.com/Effect-TS/effect/commit/a8588f99a43dc9b85dacb14f7be5d3e1ed994d4f), [`f17eb0a`](https://github.com/Effect-TS/effect/commit/f17eb0a6f669e09647c5ded5832ea6baac596659), [`f30cbfe`](https://github.com/Effect-TS/effect/commit/f30cbfe53951128ad8124ae733f12423dbc78bd8), [`78cc9c0`](https://github.com/Effect-TS/effect/commit/78cc9c0d0f36b59f7034b2ace56a4a5c02b7a551), [`ccbdbd5`](https://github.com/Effect-TS/effect/commit/ccbdbd51e7bdb114302701fbc03021868b71956a), [`a63dcbf`](https://github.com/Effect-TS/effect/commit/a63dcbf04e5c3d8d934a41bc6122e9951b1cefa9), [`b845b18`](https://github.com/Effect-TS/effect/commit/b845b186379106c17ed4b1149e2462cd0d2d77b5), [`381b794`](https://github.com/Effect-TS/effect/commit/381b79464f8e0a520585fdcfdd3e95e0553d0b27), [`4ffcaf4`](https://github.com/Effect-TS/effect/commit/4ffcaf4c4a94d3d9d5715db91aa37d7f5d9ecd99), [`ba2fd82`](https://github.com/Effect-TS/effect/commit/ba2fd822e9618f30d0f430ae4eed2eaa4813e2e3), [`02be94c`](https://github.com/Effect-TS/effect/commit/02be94ce78a3c9fc1e4c516d37d8e11e1c0eef2e), [`115d8c2`](https://github.com/Effect-TS/effect/commit/115d8c22599640ece2fd6a10564925b1d79f8a8c), [`1452635`](https://github.com/Effect-TS/effect/commit/14526354f3d0288bd1d0a85c8d4aeee8abe2cd9b), [`77f85fe`](https://github.com/Effect-TS/effect/commit/77f85fe1613348f5c990016b49dc97e252576c82), [`a3f2b31`](https://github.com/Effect-TS/effect/commit/a3f2b319343f28edb028270f7074066cea0ec429), [`310f8d3`](https://github.com/Effect-TS/effect/commit/310f8d3424a025ce1f1cc5c338232db2362052d8), [`291d616`](https://github.com/Effect-TS/effect/commit/291d616282b666bbba3664143fa1d19e839a6a5d), [`48dbbb2`](https://github.com/Effect-TS/effect/commit/48dbbb22bb2e04f63992fc267dd711a0cbd76d72), [`b43bfd6`](https://github.com/Effect-TS/effect/commit/b43bfd644a3205f1518abbfd006f761e31b3a5f9), [`1c89c78`](https://github.com/Effect-TS/effect/commit/1c89c78f9faa3638f398772f3ed7182aa97a1edd), [`9bbe1a5`](https://github.com/Effect-TS/effect/commit/9bbe1a5940eee4642ffe854356128b617e0a064c), [`dd99ab0`](https://github.com/Effect-TS/effect/commit/dd99ab007e3352761187dae330d52f65feeff7c0), [`8f397ed`](https://github.com/Effect-TS/effect/commit/8f397ed4e7852026591819cc4c8c1995bdd2fb8c), [`d7ae6b6`](https://github.com/Effect-TS/effect/commit/d7ae6b6491a88f2710612bfcddaf608ebe925f7c), [`8cf1203`](https://github.com/Effect-TS/effect/commit/8cf12030e8a3e1e76384884d41afa96e2bd72b13), [`87654c5`](https://github.com/Effect-TS/effect/commit/87654c5eed150387f4eac7153751b6085616ba52), [`f05ae0b`](https://github.com/Effect-TS/effect/commit/f05ae0b3fd2cfc7f12b668af244b3cf3fae59bc5), [`436f5eb`](https://github.com/Effect-TS/effect/commit/436f5eb44aa234bdf1de37b07da2ce8dc31ad4ec), [`d8ff960`](https://github.com/Effect-TS/effect/commit/d8ff9605bb5ef7b0cacd80e3b43ed1ce9e3d5e36), [`8766475`](https://github.com/Effect-TS/effect/commit/876647502300b9fb79d163de10fd497716d93be0), [`b64f406`](https://github.com/Effect-TS/effect/commit/b64f406de13a1b9da4664dbde2dbddaa5f776a05), [`4697aaa`](https://github.com/Effect-TS/effect/commit/4697aaa1d38f9da7fe0bcb62f708183f766fe496), [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494), [`9956f0e`](https://github.com/Effect-TS/effect/commit/9956f0e6f47096a6e31305ec2c7320b8e1a140af), [`5c7eed0`](https://github.com/Effect-TS/effect/commit/5c7eed0c5aa0988716206dd374359223edf828ee), [`183c2ea`](https://github.com/Effect-TS/effect/commit/183c2ea65ee1e9b967c746aa1b0bf2b8d3422de2), [`1e92dbd`](https://github.com/Effect-TS/effect/commit/1e92dbd26f5a74bb32103cbaac0f09460b7954de), [`4eb0fa7`](https://github.com/Effect-TS/effect/commit/4eb0fa72302b0a63ac34c6515a9cdf96338e586a), [`534b8b9`](https://github.com/Effect-TS/effect/commit/534b8b9dba195ec38a4795fe564d5e0876cb6468), [`b76a1cf`](https://github.com/Effect-TS/effect/commit/b76a1cf32b0a742c24c1874137e2da8fed5c48eb), [`acc1e53`](https://github.com/Effect-TS/effect/commit/acc1e5384aa5cb7b072f60308013f078b0099bdf), [`4950a91`](https://github.com/Effect-TS/effect/commit/4950a914a68306531a1111d4cdc4504aa8d3854e), [`c020987`](https://github.com/Effect-TS/effect/commit/c020987a0611c9e610a7eef347648b60026745b8), [`abe95d1`](https://github.com/Effect-TS/effect/commit/abe95d12340e1dd435e5df623b1967bf01938c94), [`027ceb9`](https://github.com/Effect-TS/effect/commit/027ceb91e003fbc36f0559f72e1c614cd9198c1a), [`3d203b7`](https://github.com/Effect-TS/effect/commit/3d203b702e26687d2fea4aaae7cc01dcfb82c05e), [`a29b8f4`](https://github.com/Effect-TS/effect/commit/a29b8f4de5aee58dd9ee645365c04cfe1ea6fa9f), [`ce4aa65`](https://github.com/Effect-TS/effect/commit/ce4aa65409e8500908fb6496eca06270ef5b636f), [`a8ea807`](https://github.com/Effect-TS/effect/commit/a8ea80799a0bf187c577e052fef0b7245494c7bf), [`a3ebb7f`](https://github.com/Effect-TS/effect/commit/a3ebb7fcb208f3933a40a0fd949dbcfdf1ca2921), [`473bd81`](https://github.com/Effect-TS/effect/commit/473bd814a97dba66070435cfe84933bcc143effe), [`53843f6`](https://github.com/Effect-TS/effect/commit/53843f6490f4eebaf1eeb91fdcc5f1c542b0e132), [`c5eca65`](https://github.com/Effect-TS/effect/commit/c5eca6539f0d605acd5ce19437fa88f167d69bb7), [`d6f9eba`](https://github.com/Effect-TS/effect/commit/d6f9eba7e8df0c8bd74762c313402884d94ba146), [`8d1e97a`](https://github.com/Effect-TS/effect/commit/8d1e97adbf5a36b4b53ab56f797c2ec0267a8821), [`b28ab48`](https://github.com/Effect-TS/effect/commit/b28ab48be7cf7f2bd30f9467ccac0ab41260f7ad), [`9960708`](https://github.com/Effect-TS/effect/commit/9960708052fcaaa873c471aea144ca46955e6023), [`8ac53b6`](https://github.com/Effect-TS/effect/commit/8ac53b6ae677f06d88d7d81f932ab5ff9b59a10e), [`84ad49a`](https://github.com/Effect-TS/effect/commit/84ad49a357ef156b4ff46cefc945efbe3e292eff), [`72cfa24`](https://github.com/Effect-TS/effect/commit/72cfa2420977919ba57a8e6de538e01e06d5f380), [`95c2581`](https://github.com/Effect-TS/effect/commit/95c2581e0f78e1537aad21764fe562206ca50adb), [`d5c7cd2`](https://github.com/Effect-TS/effect/commit/d5c7cd2c47b2b01523aaea48d2dd039b89b36114), [`fe4fed1`](https://github.com/Effect-TS/effect/commit/fe4fed1b014c78043a8efe37520c527100910010), [`d150a64`](https://github.com/Effect-TS/effect/commit/d150a643c4f60da2dc19ae813b807d48b580087e), [`05b1e80`](https://github.com/Effect-TS/effect/commit/05b1e80cde3a8da1ff1afe288f8539b72c05bb87), [`414dc90`](https://github.com/Effect-TS/effect/commit/414dc9056a39aa22f7cc27aeb5e631231ce50982), [`3f51acd`](https://github.com/Effect-TS/effect/commit/3f51acdf5e2490d7d0a833cd67ce06d1d867facd), [`d68ff05`](https://github.com/Effect-TS/effect/commit/d68ff05506ab5bb3ec07e1b759ce296423b0dbbe), [`f3cf1e6`](https://github.com/Effect-TS/effect/commit/f3cf1e6c36f1aac8399d9b540aecb6410be3096f), [`6525771`](https://github.com/Effect-TS/effect/commit/6525771eef7df6b71a3ef6e299ee93d798ed3dda), [`4ab4e83`](https://github.com/Effect-TS/effect/commit/4ab4e838a4c9ab3c9cc0d52d3e83de88686601b0), [`f7f1d78`](https://github.com/Effect-TS/effect/commit/f7f1d78e52023bdfbebf76949c550db8bb9411dd), [`47b358a`](https://github.com/Effect-TS/effect/commit/47b358a4b85d22973a7957abd1a859f65b75b7da), [`45ffa72`](https://github.com/Effect-TS/effect/commit/45ffa7221259c7e4be9797b90f6ca569fe7ec8a3), [`6232650`](https://github.com/Effect-TS/effect/commit/62326505a74fcd8fe27c8e2064128d2dea484ecc), [`4b73e1b`](https://github.com/Effect-TS/effect/commit/4b73e1bd5576346d7939be8915f3053e428d1c0e), [`284050c`](https://github.com/Effect-TS/effect/commit/284050c2aa6e78eceb08e5eff1803501cc80a658), [`c85fc0b`](https://github.com/Effect-TS/effect/commit/c85fc0b8f2195780929d6fc8ecdc084faf18736c), [`7999b07`](https://github.com/Effect-TS/effect/commit/7999b078950fda15e235db1e61e8db5eb0632528), [`7d455f5`](https://github.com/Effect-TS/effect/commit/7d455f51acf2f0e9f710f298e14e4d81179a9259), [`d681c2e`](https://github.com/Effect-TS/effect/commit/d681c2eb1bb07d782540286368e8b42533df3063), [`84d2a47`](https://github.com/Effect-TS/effect/commit/84d2a470ecc0fcad7f1d1e7a50464e02af0e6972), [`ed74b18`](https://github.com/Effect-TS/effect/commit/ed74b18b95b0797808f2f7928f413f1cf2279d77), [`2245997`](https://github.com/Effect-TS/effect/commit/2245997ad2214591b7d9674777595092926743cf), [`d386979`](https://github.com/Effect-TS/effect/commit/d386979fedbdd433d28816f9cc74675a9af45189), [`fc9fedf`](https://github.com/Effect-TS/effect/commit/fc9fedf59fc26a8bc6e45c3860a921f3b7e0f971), [`7750dbe`](https://github.com/Effect-TS/effect/commit/7750dbe0d5b9929c6d92b0365b42784b993ea2a6), [`fc91af6`](https://github.com/Effect-TS/effect/commit/fc91af659d0682659e6347eede288f90f618acc0), [`39b9738`](https://github.com/Effect-TS/effect/commit/39b9738727ab4d87298fd640edd138f352c70ee3), [`88093b5`](https://github.com/Effect-TS/effect/commit/88093b57951a1b597599fe4772ab690b36a95818), [`d14c463`](https://github.com/Effect-TS/effect/commit/d14c4631ac82911a8ca0b6c24c14ecb95863ee6b), [`d473bd3`](https://github.com/Effect-TS/effect/commit/d473bd3ee966a3a0c245d0e3c752fd91368b8ff6), [`84864bc`](https://github.com/Effect-TS/effect/commit/84864bc30c9e92a1226f65bb78b0641a7e0acea2), [`e2ae724`](https://github.com/Effect-TS/effect/commit/e2ae72481e755a454f28ce45e90f43b857b7c6e5), [`f921ed3`](https://github.com/Effect-TS/effect/commit/f921ed305c63415dc468e94d9319cc3237119138), [`1df933d`](https://github.com/Effect-TS/effect/commit/1df933d1dee2e09a25f9c27214b31aa94fa9d020), [`829aff9`](https://github.com/Effect-TS/effect/commit/829aff97d71d7900b7765ba2f63e33874b898cdc), [`aa0aba3`](https://github.com/Effect-TS/effect/commit/aa0aba383382ca1b99fbe618dceba63c3a5a6a53), [`a71140f`](https://github.com/Effect-TS/effect/commit/a71140fbe79809aa5311a55b1c6908905dc2df26), [`0276a27`](https://github.com/Effect-TS/effect/commit/0276a27ddbb15d163daf56f059ca9f62c4fd9939), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`c86c999`](https://github.com/Effect-TS/effect/commit/c86c99939e1628813a2f92f0906cfbbe924504e8), [`975f758`](https://github.com/Effect-TS/effect/commit/975f7586f3dcda5743d6570a740013a9fbdce9a7), [`2a3a478`](https://github.com/Effect-TS/effect/commit/2a3a4783bcd2d4e91e3505c7a6c1e398fea89595), [`1e6e206`](https://github.com/Effect-TS/effect/commit/1e6e206f6327a114f55a541775f465c0bc9fbf3e), [`fa6027b`](https://github.com/Effect-TS/effect/commit/fa6027b16e65376078e526fa33116ac0cc82485b), [`7616f73`](https://github.com/Effect-TS/effect/commit/7616f73c16bd45a3a3e2202d3f81c6d1cc63f4e0), [`fc668b6`](https://github.com/Effect-TS/effect/commit/fc668b61e9755545a47582dee6c4425d5652cbb6), [`d425c8c`](https://github.com/Effect-TS/effect/commit/d425c8c1319519e861714bcb9d055b63a9434fad), [`ce120f4`](https://github.com/Effect-TS/effect/commit/ce120f474b87d8a5a787dd7f848177ecd3b960fc), [`248201f`](https://github.com/Effect-TS/effect/commit/248201f7846d855685a4dc1b2f01968981706d69), [`e80d397`](https://github.com/Effect-TS/effect/commit/e80d3972fcca4e986b6497d67406f45ee1be540d), [`f1a941d`](https://github.com/Effect-TS/effect/commit/f1a941d52c2f71973e998883869e4e9ef510abf2), [`73bc3a1`](https://github.com/Effect-TS/effect/commit/73bc3a13b6e1b7a060e6bcd19ae7796443211a80), [`b628bb1`](https://github.com/Effect-TS/effect/commit/b628bb1240a09df2244c3ff6c587f978feb25225), [`6e3ae7b`](https://github.com/Effect-TS/effect/commit/6e3ae7b6359bda08cea74b761fd54224d7af45e2), [`e891247`](https://github.com/Effect-TS/effect/commit/e8912473bd4128714a0f8cb04621f3e12f62a910), [`53e6c73`](https://github.com/Effect-TS/effect/commit/53e6c7311b948a62e93e57f6daee66a8ec24b030), [`1579d6f`](https://github.com/Effect-TS/effect/commit/1579d6fd063e444e4fcdfd08ff194c7485203f9c), [`d3c6b73`](https://github.com/Effect-TS/effect/commit/d3c6b73cc4b78f22fdb2e25c8bebda027551540b), [`4a59c6a`](https://github.com/Effect-TS/effect/commit/4a59c6a559e69d399b00c6acd48c8e1977e34875), [`145d8e1`](https://github.com/Effect-TS/effect/commit/145d8e1013220425b8edf34f7011c73f73e1cdcf), [`f74282c`](https://github.com/Effect-TS/effect/commit/f74282cb08de808b678ce67687acbcd61939d2f6), [`0a38623`](https://github.com/Effect-TS/effect/commit/0a3862349ebfbc41746aa0de5e682992719c1da6), [`0a08ae0`](https://github.com/Effect-TS/effect/commit/0a08ae0626f8124779ec2e32ae2088d28db157c6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`d60c5d4`](https://github.com/Effect-TS/effect/commit/d60c5d40b5954ea1557aef1502b4b87f98bbf134), [`07ffd25`](https://github.com/Effect-TS/effect/commit/07ffd25bd0e82c9e6b72b8e669833fb4531a9da3), [`9b517ad`](https://github.com/Effect-TS/effect/commit/9b517ad28a0c2f213693bcab408658c40e3b6d9b), [`604b1c1`](https://github.com/Effect-TS/effect/commit/604b1c109d6c9e654b9b7fcc98d6b80361400325), [`ccc2e02`](https://github.com/Effect-TS/effect/commit/ccc2e02def5965f5535ca97fac2051f4387c4e13), [`14d810a`](https://github.com/Effect-TS/effect/commit/14d810a726caad8a7b0d42f04a6876f25fbcc005), [`a9d1ee3`](https://github.com/Effect-TS/effect/commit/a9d1ee3d4d51e97ea33440fe6100935d5fd5aada), [`a31adbe`](https://github.com/Effect-TS/effect/commit/a31adbefd6c04c70536de11433edba9edd1f5013), [`7245f87`](https://github.com/Effect-TS/effect/commit/7245f87ace09806d15893df8f0bac8f0b9b817db), [`3a0828b`](https://github.com/Effect-TS/effect/commit/3a0828b058abb4b4646bd880458b4e636d77d1c3), [`cd83544`](https://github.com/Effect-TS/effect/commit/cd83544dd594b99e8ea0d7a7645c086f5861769b), [`6550a07`](https://github.com/Effect-TS/effect/commit/6550a074c5c501d8a95dcd354f17e9785a1ae1eb), [`6f090d4`](https://github.com/Effect-TS/effect/commit/6f090d4c2032cc35f7f9a9eac900d0329b8ac07e), [`b505c0d`](https://github.com/Effect-TS/effect/commit/b505c0db03e86eb126237690a5867f457eb4237f), [`186dd49`](https://github.com/Effect-TS/effect/commit/186dd4914084fa346a870ce9c637b9c2a6cc8100), [`9f37e58`](https://github.com/Effect-TS/effect/commit/9f37e58d00e06150d96af48fffe780947ca0d519), [`4446451`](https://github.com/Effect-TS/effect/commit/44464510114773e1ebd1abb6efffa382cf69998e), [`58be972`](https://github.com/Effect-TS/effect/commit/58be97238ee1f6cd26f6a3a645d1df97a091a224), [`1320075`](https://github.com/Effect-TS/effect/commit/1320075fd4ed381f60f6f37f280dbec17cb29b81), [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80), [`f7490d4`](https://github.com/Effect-TS/effect/commit/f7490d49c75b99118c6c42549511ffa166459f59), [`c8ea602`](https://github.com/Effect-TS/effect/commit/c8ea60231ad69e23c0bf319f6f6b3ff28fc45e4b), [`62d82f4`](https://github.com/Effect-TS/effect/commit/62d82f4919c72d8cc0cfd2b704ed6900f69ad9a8), [`a2c9e7c`](https://github.com/Effect-TS/effect/commit/a2c9e7c17a4118b74a1eab3332042dafe0bb7318), [`97dd022`](https://github.com/Effect-TS/effect/commit/97dd022fe7fbd1696dd60f3319eae594abdd3760), [`f984ee8`](https://github.com/Effect-TS/effect/commit/f984ee82d255c7958eeed6f0dd08de1b88cbbfd6), [`f1b2910`](https://github.com/Effect-TS/effect/commit/f1b291027c3055f801c2f288dda21ba84c361956), [`a29e05a`](https://github.com/Effect-TS/effect/commit/a29e05a907053a8617763b4e7408e1f012b59049), [`1aa1d8b`](https://github.com/Effect-TS/effect/commit/1aa1d8bd0f2c2d429938657927f34f8ff0f940a0), [`bb99734`](https://github.com/Effect-TS/effect/commit/bb99734ef87bcc377c628bc4072aa856ce5f7ca6), [`222e7ca`](https://github.com/Effect-TS/effect/commit/222e7caa4bb0da6d81a4eabf25dffc181c20f6a3), [`797c9e3`](https://github.com/Effect-TS/effect/commit/797c9e3c1f2e66db1570cf6aa5c4a15e94507bf0), [`b4d5398`](https://github.com/Effect-TS/effect/commit/b4d5398598c04a84054a55873c943d587880058d), [`c8349ed`](https://github.com/Effect-TS/effect/commit/c8349ede1a2add807e9ec9c37612b3a39bf284f8), [`8426e5f`](https://github.com/Effect-TS/effect/commit/8426e5f7ad11f15e39565cf8ba0874bf974f5085), [`26e0085`](https://github.com/Effect-TS/effect/commit/26e0085d098f6e526c085c357b8d82ee068b6518), [`1a86166`](https://github.com/Effect-TS/effect/commit/1a86166f102b517ee4d89dca0095e28f738eee10), [`0856631`](https://github.com/Effect-TS/effect/commit/0856631a89d8f72d206c366905ddac75468a8e85), [`0af0985`](https://github.com/Effect-TS/effect/commit/0af0985d5c132b33e14a2df59e0567d7ba2a1710), [`bc582c9`](https://github.com/Effect-TS/effect/commit/bc582c94313cbb89d87b3844ffffabebbb4ffa83), [`2c63f1e`](https://github.com/Effect-TS/effect/commit/2c63f1e07e375b7958e14929c6a4b358c819a9d0), [`0d98213`](https://github.com/Effect-TS/effect/commit/0d9821346453d44696258103643d5482e63638b4), [`ca6f0dc`](https://github.com/Effect-TS/effect/commit/ca6f0dc4fcea564b351bf505df015d16a8b965ed), [`d8cc9ed`](https://github.com/Effect-TS/effect/commit/d8cc9ed80e1df9c056a46ba62df01f7fc0f531c3), [`42fd969`](https://github.com/Effect-TS/effect/commit/42fd969b45025eed6d60cb87b97c5ff88d187671), [`5641ad3`](https://github.com/Effect-TS/effect/commit/5641ad333a88bb9e56baf886006682678391be0a), [`629870d`](https://github.com/Effect-TS/effect/commit/629870d461b36d3dacb30c8d203b8d20d4beaba0), [`d592c14`](https://github.com/Effect-TS/effect/commit/d592c1492ad895ea72537e10387b735f3ba0e7de), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`53909a9`](https://github.com/Effect-TS/effect/commit/53909a9bf14b29725a08dcccde02bee8e95a6b14), [`78a4269`](https://github.com/Effect-TS/effect/commit/78a42697f6085c87c09b18e8095bb7541bd65261), [`06c6307`](https://github.com/Effect-TS/effect/commit/06c6307d3d5538bc398c67a44239bc3d52ab1a4d), [`0847c41`](https://github.com/Effect-TS/effect/commit/0847c41e2472a347cd8f77d460d3cbdfecd4adf0), [`5a77084`](https://github.com/Effect-TS/effect/commit/5a77084651bc58617b9988606473cf9ba5ca0032), [`96f99b3`](https://github.com/Effect-TS/effect/commit/96f99b325da32bfa535e826d89d838d0edec0dea), [`7bb8781`](https://github.com/Effect-TS/effect/commit/7bb878176b8ad20cf0e671b570caf72e194f9515), [`4907e9b`](https://github.com/Effect-TS/effect/commit/4907e9b2470894ddaf0cbdec2612b59f04a51dbe), [`2a30248`](https://github.com/Effect-TS/effect/commit/2a30248b6eb739f22403456209bc468f2f4ef26a), [`8364ddd`](https://github.com/Effect-TS/effect/commit/8364ddda233d35ee472cc2356312317a60a148a2), [`ad67d8c`](https://github.com/Effect-TS/effect/commit/ad67d8c5a1b195ff5468a9f2e963c59aa0b3166f), [`ec0c087`](https://github.com/Effect-TS/effect/commit/ec0c0875262fb81c7992f5023776b8fa894c49b6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`a2c1ce6`](https://github.com/Effect-TS/effect/commit/a2c1ce62f04214d98d848f4cfe33d677e1a87d39), [`91e9af0`](https://github.com/Effect-TS/effect/commit/91e9af0d5f138933d3a131ca1cfbc52d3bc55481), [`7bd3f34`](https://github.com/Effect-TS/effect/commit/7bd3f34385a06157b249a351e32ffdcbe1cf2c37), [`ef16581`](https://github.com/Effect-TS/effect/commit/ef165814eae0a199fdca5b3c2839bb4fcdfd5b5d), [`1693a87`](https://github.com/Effect-TS/effect/commit/1693a877324273f5a23fa7d7fc892f4ee12f20c2), [`df3fc47`](https://github.com/Effect-TS/effect/commit/df3fc47609eda7843dae2b557c8fe834299e26b4), [`e11be41`](https://github.com/Effect-TS/effect/commit/e11be41dfc73fa6b5643ab9af7df18d6907927d7), [`2e39e8b`](https://github.com/Effect-TS/effect/commit/2e39e8bafef7d3fee9f3b324b7d50cc064376667), [`11c5ee7`](https://github.com/Effect-TS/effect/commit/11c5ee7202acd3bb789d0a8ce78e6c4523071553), [`1742d2f`](https://github.com/Effect-TS/effect/commit/1742d2f48441cb7b1a8d9aca3a8e6f0acbc35a96), [`8efc70e`](https://github.com/Effect-TS/effect/commit/8efc70ea16bee43fd9adde2871cf82a8131b80a0), [`9642776`](https://github.com/Effect-TS/effect/commit/964277661423f398f777ec2eab4b3ecc0b53f53b), [`6680828`](https://github.com/Effect-TS/effect/commit/668082885f52672e4c7fad4e0fe2bfa0e69703e2), [`c34edcb`](https://github.com/Effect-TS/effect/commit/c34edcb6d7a6151d53e19828c92abf7dd8450c2a), [`7b2c5bd`](https://github.com/Effect-TS/effect/commit/7b2c5bd3c394fa4751fb229394ac9c03e401f441), [`1a2ccee`](https://github.com/Effect-TS/effect/commit/1a2ccee2bbb93514dc66e0a9cdeb52a82172ab0e), [`db995df`](https://github.com/Effect-TS/effect/commit/db995df19b0381b64cc39b50aa28d7bd8f2c1294), [`af0ccdd`](https://github.com/Effect-TS/effect/commit/af0ccdd13aa8917bce7161b8bc257fbe51c6a6a5), [`addeaea`](https://github.com/Effect-TS/effect/commit/addeaea0b0abe0dc24ca1d4d0fd194a0e458be70), [`7704034`](https://github.com/Effect-TS/effect/commit/770403411bcd8befef0552a0e40066abc417f6ea), [`e72b12f`](https://github.com/Effect-TS/effect/commit/e72b12fc305710550bc6dcb978e92de8abff88cd), [`310dd9c`](https://github.com/Effect-TS/effect/commit/310dd9ce9681e97f558bb042adb35f6040ab81e3), [`1c2afc1`](https://github.com/Effect-TS/effect/commit/1c2afc15bbf4ce5a91d24da99bc275b5bd744e77), [`44f44ca`](https://github.com/Effect-TS/effect/commit/44f44cae65249801c082c3a3848016eeeac5eb24), [`f43b9d6`](https://github.com/Effect-TS/effect/commit/f43b9d6d99cb591c0377bb1474121827201627c9), [`56e72b3`](https://github.com/Effect-TS/effect/commit/56e72b38c01bbe6ec921ad151f83b7849444f219), [`50ef80e`](https://github.com/Effect-TS/effect/commit/50ef80e251b85b5ee63aed06fe0244b5433ec2fa), [`fc3b718`](https://github.com/Effect-TS/effect/commit/fc3b7187114a31cbb21a250f55fda601bb3edcf0), [`ee336d8`](https://github.com/Effect-TS/effect/commit/ee336d80b4024b51654e7a2b8d869d244153fcdf), [`d12f922`](https://github.com/Effect-TS/effect/commit/d12f922853eff2310dbe9fc9b31562ad81b38435), [`46d8310`](https://github.com/Effect-TS/effect/commit/46d83101e8408c0428d9a8760e06e23a69ad6070), [`59812fd`](https://github.com/Effect-TS/effect/commit/59812fd221cde16fc77c03d326c6a2cf8fbec6b9), [`f9d0dec`](https://github.com/Effect-TS/effect/commit/f9d0dec12c5f229f05d7a74d532a85c6bbffefbf), [`4372c79`](https://github.com/Effect-TS/effect/commit/4372c79a32fcecc7b6b284d49a215b2c4fef9d77), [`81485ef`](https://github.com/Effect-TS/effect/commit/81485ef0288a3321e75d7d4ae32b7e0e06b6f86b), [`bd393d6`](https://github.com/Effect-TS/effect/commit/bd393d63c19bdd0ab212d95576cec89051c8501c), [`0c95c04`](https://github.com/Effect-TS/effect/commit/0c95c04b41c2d5d09e4564fd997572207fd72595)]: + - effect@4.0.0-rc.113 + ## 4.0.0-rc.112 ### Patch Changes diff --git a/repos/effect/packages/atom/solid/package.json b/repos/effect/packages/atom/solid/package.json index 0bfe68b26..f2feb710d 100644 --- a/repos/effect/packages/atom/solid/package.json +++ b/repos/effect/packages/atom/solid/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-solid", - "version": "4.0.0-rc.112", + "version": "4.0.0-rc.117", "type": "module", "license": "MIT", "description": "SolidJS bindings for the Effect Atom modules", @@ -65,10 +65,10 @@ }, "devDependencies": { "@solidjs/testing-library": "^0.8.10", - "@testing-library/dom": "^10.4.1", + "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "effect": "workspace:^", - "jsdom": "^30.0.1", + "jsdom": "~30.0.1", "solid-js": "^1.9.15" } } diff --git a/repos/effect/packages/atom/solid/src/RegistryContext.ts b/repos/effect/packages/atom/solid/src/RegistryContext.ts index 7c1b6c0f2..111cfab28 100644 --- a/repos/effect/packages/atom/solid/src/RegistryContext.ts +++ b/repos/effect/packages/atom/solid/src/RegistryContext.ts @@ -68,7 +68,7 @@ export const RegistryProvider = (options: { scheduleTask: options.scheduleTask, initialValues: options.initialValues, timeoutResolution: options.timeoutResolution, - defaultIdleTTL: options.defaultIdleTTL ?? 400 + defaultIdleTTL: options.defaultIdleTTL }) onCleanup(() => registry.dispose()) return createComponent(RegistryContext.Provider, { diff --git a/repos/effect/packages/atom/vue/CHANGELOG.md b/repos/effect/packages/atom/vue/CHANGELOG.md index 0cfa84462..9d5309f25 100644 --- a/repos/effect/packages/atom/vue/CHANGELOG.md +++ b/repos/effect/packages/atom/vue/CHANGELOG.md @@ -1,5 +1,41 @@ # @effect/atom-vue +## 4.0.0-rc.117 + +### Patch Changes + +- Updated dependencies [[`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9), [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724), [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0), [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b), [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9), [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a), [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca), [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1), [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5), [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741)]: + - effect@4.0.0-rc.117 + +## 4.0.0-rc.116 + +### Patch Changes + +- Updated dependencies [[`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f), [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2), [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d), [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356), [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf), [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b), [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f), [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a), [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac), [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06), [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8), [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153), [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537), [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746), [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9), [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c), [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3), [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b), [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b), [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72), [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f), [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5), [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55), [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b)]: + - effect@4.0.0-rc.116 + +## 4.0.0-rc.115 + +### Patch Changes + +- Updated dependencies [[`657254b`](https://github.com/Effect-TS/effect/commit/657254b8218628b0116497d09aaf783cb88279f3), [`f9ef0e9`](https://github.com/Effect-TS/effect/commit/f9ef0e970260b5de143392d00593e6c8b4614cff), [`4f73f9e`](https://github.com/Effect-TS/effect/commit/4f73f9e0bd9538260f0f7166e0e6abdbd507a0e8)]: + - effect@4.0.0-rc.115 + +## 4.0.0-rc.114 + +### Patch Changes + +- Updated dependencies [[`3ff4952`](https://github.com/Effect-TS/effect/commit/3ff49521afa6fe6c85c2c230f7e986f7374ef353), [`6d55555`](https://github.com/Effect-TS/effect/commit/6d55555b83701c42855758944985a6fdbf57fffe), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`d4e4ad5`](https://github.com/Effect-TS/effect/commit/d4e4ad57d06d0a30d5e8090ed951f080bcd3a3d7), [`b1988f4`](https://github.com/Effect-TS/effect/commit/b1988f496eef07482f10f4cc03ef20b9570ea0ac), [`482b7d7`](https://github.com/Effect-TS/effect/commit/482b7d7eb08ebe6bc57781414a5a9a12600b2c71), [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3), [`9941e6d`](https://github.com/Effect-TS/effect/commit/9941e6dbf800cfd3723bfb421306e04e36b5882d)]: + - effect@4.0.0-rc.114 + +## 4.0.0-rc.113 + +### Patch Changes + +- [#7746](https://github.com/Effect-TS/effect/pull/7746) [`74ff5c3`](https://github.com/Effect-TS/effect/commit/74ff5c31380db8816006bad7031e0a76032dcc08) Thanks @kitlangton! - Fix `useAtomRef` returning a stale value after switching refs. +- Updated dependencies [[`49e3901`](https://github.com/Effect-TS/effect/commit/49e39010ff84acbde32c50d559a2f76426cf830e), [`b945ded`](https://github.com/Effect-TS/effect/commit/b945ded23aa9a0ad88bb55aa4089680866dccf92), [`d6422f4`](https://github.com/Effect-TS/effect/commit/d6422f4104062e3e176abe92ef0e444cd4cc6bab), [`5a80204`](https://github.com/Effect-TS/effect/commit/5a802043984727b0c5a291af39d1b9bbfa8d7b8b), [`53511ef`](https://github.com/Effect-TS/effect/commit/53511efcd1c7a05d34b4c9a19c19ed317ebc5d4c), [`79ae49f`](https://github.com/Effect-TS/effect/commit/79ae49f19cab9a42c8f27ae8b104d5e39e6131fa), [`0d083ba`](https://github.com/Effect-TS/effect/commit/0d083ba26b2e1afec8d3e8d83db0d05683b6602b), [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803), [`debe8fd`](https://github.com/Effect-TS/effect/commit/debe8fdc15d00d51cf707353365d7b877123eec6), [`a8588f9`](https://github.com/Effect-TS/effect/commit/a8588f99a43dc9b85dacb14f7be5d3e1ed994d4f), [`f17eb0a`](https://github.com/Effect-TS/effect/commit/f17eb0a6f669e09647c5ded5832ea6baac596659), [`f30cbfe`](https://github.com/Effect-TS/effect/commit/f30cbfe53951128ad8124ae733f12423dbc78bd8), [`78cc9c0`](https://github.com/Effect-TS/effect/commit/78cc9c0d0f36b59f7034b2ace56a4a5c02b7a551), [`ccbdbd5`](https://github.com/Effect-TS/effect/commit/ccbdbd51e7bdb114302701fbc03021868b71956a), [`a63dcbf`](https://github.com/Effect-TS/effect/commit/a63dcbf04e5c3d8d934a41bc6122e9951b1cefa9), [`b845b18`](https://github.com/Effect-TS/effect/commit/b845b186379106c17ed4b1149e2462cd0d2d77b5), [`381b794`](https://github.com/Effect-TS/effect/commit/381b79464f8e0a520585fdcfdd3e95e0553d0b27), [`4ffcaf4`](https://github.com/Effect-TS/effect/commit/4ffcaf4c4a94d3d9d5715db91aa37d7f5d9ecd99), [`ba2fd82`](https://github.com/Effect-TS/effect/commit/ba2fd822e9618f30d0f430ae4eed2eaa4813e2e3), [`02be94c`](https://github.com/Effect-TS/effect/commit/02be94ce78a3c9fc1e4c516d37d8e11e1c0eef2e), [`115d8c2`](https://github.com/Effect-TS/effect/commit/115d8c22599640ece2fd6a10564925b1d79f8a8c), [`1452635`](https://github.com/Effect-TS/effect/commit/14526354f3d0288bd1d0a85c8d4aeee8abe2cd9b), [`77f85fe`](https://github.com/Effect-TS/effect/commit/77f85fe1613348f5c990016b49dc97e252576c82), [`a3f2b31`](https://github.com/Effect-TS/effect/commit/a3f2b319343f28edb028270f7074066cea0ec429), [`310f8d3`](https://github.com/Effect-TS/effect/commit/310f8d3424a025ce1f1cc5c338232db2362052d8), [`291d616`](https://github.com/Effect-TS/effect/commit/291d616282b666bbba3664143fa1d19e839a6a5d), [`48dbbb2`](https://github.com/Effect-TS/effect/commit/48dbbb22bb2e04f63992fc267dd711a0cbd76d72), [`b43bfd6`](https://github.com/Effect-TS/effect/commit/b43bfd644a3205f1518abbfd006f761e31b3a5f9), [`1c89c78`](https://github.com/Effect-TS/effect/commit/1c89c78f9faa3638f398772f3ed7182aa97a1edd), [`9bbe1a5`](https://github.com/Effect-TS/effect/commit/9bbe1a5940eee4642ffe854356128b617e0a064c), [`dd99ab0`](https://github.com/Effect-TS/effect/commit/dd99ab007e3352761187dae330d52f65feeff7c0), [`8f397ed`](https://github.com/Effect-TS/effect/commit/8f397ed4e7852026591819cc4c8c1995bdd2fb8c), [`d7ae6b6`](https://github.com/Effect-TS/effect/commit/d7ae6b6491a88f2710612bfcddaf608ebe925f7c), [`8cf1203`](https://github.com/Effect-TS/effect/commit/8cf12030e8a3e1e76384884d41afa96e2bd72b13), [`87654c5`](https://github.com/Effect-TS/effect/commit/87654c5eed150387f4eac7153751b6085616ba52), [`f05ae0b`](https://github.com/Effect-TS/effect/commit/f05ae0b3fd2cfc7f12b668af244b3cf3fae59bc5), [`436f5eb`](https://github.com/Effect-TS/effect/commit/436f5eb44aa234bdf1de37b07da2ce8dc31ad4ec), [`d8ff960`](https://github.com/Effect-TS/effect/commit/d8ff9605bb5ef7b0cacd80e3b43ed1ce9e3d5e36), [`8766475`](https://github.com/Effect-TS/effect/commit/876647502300b9fb79d163de10fd497716d93be0), [`b64f406`](https://github.com/Effect-TS/effect/commit/b64f406de13a1b9da4664dbde2dbddaa5f776a05), [`4697aaa`](https://github.com/Effect-TS/effect/commit/4697aaa1d38f9da7fe0bcb62f708183f766fe496), [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494), [`9956f0e`](https://github.com/Effect-TS/effect/commit/9956f0e6f47096a6e31305ec2c7320b8e1a140af), [`5c7eed0`](https://github.com/Effect-TS/effect/commit/5c7eed0c5aa0988716206dd374359223edf828ee), [`183c2ea`](https://github.com/Effect-TS/effect/commit/183c2ea65ee1e9b967c746aa1b0bf2b8d3422de2), [`1e92dbd`](https://github.com/Effect-TS/effect/commit/1e92dbd26f5a74bb32103cbaac0f09460b7954de), [`4eb0fa7`](https://github.com/Effect-TS/effect/commit/4eb0fa72302b0a63ac34c6515a9cdf96338e586a), [`534b8b9`](https://github.com/Effect-TS/effect/commit/534b8b9dba195ec38a4795fe564d5e0876cb6468), [`b76a1cf`](https://github.com/Effect-TS/effect/commit/b76a1cf32b0a742c24c1874137e2da8fed5c48eb), [`acc1e53`](https://github.com/Effect-TS/effect/commit/acc1e5384aa5cb7b072f60308013f078b0099bdf), [`4950a91`](https://github.com/Effect-TS/effect/commit/4950a914a68306531a1111d4cdc4504aa8d3854e), [`c020987`](https://github.com/Effect-TS/effect/commit/c020987a0611c9e610a7eef347648b60026745b8), [`abe95d1`](https://github.com/Effect-TS/effect/commit/abe95d12340e1dd435e5df623b1967bf01938c94), [`027ceb9`](https://github.com/Effect-TS/effect/commit/027ceb91e003fbc36f0559f72e1c614cd9198c1a), [`3d203b7`](https://github.com/Effect-TS/effect/commit/3d203b702e26687d2fea4aaae7cc01dcfb82c05e), [`a29b8f4`](https://github.com/Effect-TS/effect/commit/a29b8f4de5aee58dd9ee645365c04cfe1ea6fa9f), [`ce4aa65`](https://github.com/Effect-TS/effect/commit/ce4aa65409e8500908fb6496eca06270ef5b636f), [`a8ea807`](https://github.com/Effect-TS/effect/commit/a8ea80799a0bf187c577e052fef0b7245494c7bf), [`a3ebb7f`](https://github.com/Effect-TS/effect/commit/a3ebb7fcb208f3933a40a0fd949dbcfdf1ca2921), [`473bd81`](https://github.com/Effect-TS/effect/commit/473bd814a97dba66070435cfe84933bcc143effe), [`53843f6`](https://github.com/Effect-TS/effect/commit/53843f6490f4eebaf1eeb91fdcc5f1c542b0e132), [`c5eca65`](https://github.com/Effect-TS/effect/commit/c5eca6539f0d605acd5ce19437fa88f167d69bb7), [`d6f9eba`](https://github.com/Effect-TS/effect/commit/d6f9eba7e8df0c8bd74762c313402884d94ba146), [`8d1e97a`](https://github.com/Effect-TS/effect/commit/8d1e97adbf5a36b4b53ab56f797c2ec0267a8821), [`b28ab48`](https://github.com/Effect-TS/effect/commit/b28ab48be7cf7f2bd30f9467ccac0ab41260f7ad), [`9960708`](https://github.com/Effect-TS/effect/commit/9960708052fcaaa873c471aea144ca46955e6023), [`8ac53b6`](https://github.com/Effect-TS/effect/commit/8ac53b6ae677f06d88d7d81f932ab5ff9b59a10e), [`84ad49a`](https://github.com/Effect-TS/effect/commit/84ad49a357ef156b4ff46cefc945efbe3e292eff), [`72cfa24`](https://github.com/Effect-TS/effect/commit/72cfa2420977919ba57a8e6de538e01e06d5f380), [`95c2581`](https://github.com/Effect-TS/effect/commit/95c2581e0f78e1537aad21764fe562206ca50adb), [`d5c7cd2`](https://github.com/Effect-TS/effect/commit/d5c7cd2c47b2b01523aaea48d2dd039b89b36114), [`fe4fed1`](https://github.com/Effect-TS/effect/commit/fe4fed1b014c78043a8efe37520c527100910010), [`d150a64`](https://github.com/Effect-TS/effect/commit/d150a643c4f60da2dc19ae813b807d48b580087e), [`05b1e80`](https://github.com/Effect-TS/effect/commit/05b1e80cde3a8da1ff1afe288f8539b72c05bb87), [`414dc90`](https://github.com/Effect-TS/effect/commit/414dc9056a39aa22f7cc27aeb5e631231ce50982), [`3f51acd`](https://github.com/Effect-TS/effect/commit/3f51acdf5e2490d7d0a833cd67ce06d1d867facd), [`d68ff05`](https://github.com/Effect-TS/effect/commit/d68ff05506ab5bb3ec07e1b759ce296423b0dbbe), [`f3cf1e6`](https://github.com/Effect-TS/effect/commit/f3cf1e6c36f1aac8399d9b540aecb6410be3096f), [`6525771`](https://github.com/Effect-TS/effect/commit/6525771eef7df6b71a3ef6e299ee93d798ed3dda), [`4ab4e83`](https://github.com/Effect-TS/effect/commit/4ab4e838a4c9ab3c9cc0d52d3e83de88686601b0), [`f7f1d78`](https://github.com/Effect-TS/effect/commit/f7f1d78e52023bdfbebf76949c550db8bb9411dd), [`47b358a`](https://github.com/Effect-TS/effect/commit/47b358a4b85d22973a7957abd1a859f65b75b7da), [`45ffa72`](https://github.com/Effect-TS/effect/commit/45ffa7221259c7e4be9797b90f6ca569fe7ec8a3), [`6232650`](https://github.com/Effect-TS/effect/commit/62326505a74fcd8fe27c8e2064128d2dea484ecc), [`4b73e1b`](https://github.com/Effect-TS/effect/commit/4b73e1bd5576346d7939be8915f3053e428d1c0e), [`284050c`](https://github.com/Effect-TS/effect/commit/284050c2aa6e78eceb08e5eff1803501cc80a658), [`c85fc0b`](https://github.com/Effect-TS/effect/commit/c85fc0b8f2195780929d6fc8ecdc084faf18736c), [`7999b07`](https://github.com/Effect-TS/effect/commit/7999b078950fda15e235db1e61e8db5eb0632528), [`7d455f5`](https://github.com/Effect-TS/effect/commit/7d455f51acf2f0e9f710f298e14e4d81179a9259), [`d681c2e`](https://github.com/Effect-TS/effect/commit/d681c2eb1bb07d782540286368e8b42533df3063), [`84d2a47`](https://github.com/Effect-TS/effect/commit/84d2a470ecc0fcad7f1d1e7a50464e02af0e6972), [`ed74b18`](https://github.com/Effect-TS/effect/commit/ed74b18b95b0797808f2f7928f413f1cf2279d77), [`2245997`](https://github.com/Effect-TS/effect/commit/2245997ad2214591b7d9674777595092926743cf), [`d386979`](https://github.com/Effect-TS/effect/commit/d386979fedbdd433d28816f9cc74675a9af45189), [`fc9fedf`](https://github.com/Effect-TS/effect/commit/fc9fedf59fc26a8bc6e45c3860a921f3b7e0f971), [`7750dbe`](https://github.com/Effect-TS/effect/commit/7750dbe0d5b9929c6d92b0365b42784b993ea2a6), [`fc91af6`](https://github.com/Effect-TS/effect/commit/fc91af659d0682659e6347eede288f90f618acc0), [`39b9738`](https://github.com/Effect-TS/effect/commit/39b9738727ab4d87298fd640edd138f352c70ee3), [`88093b5`](https://github.com/Effect-TS/effect/commit/88093b57951a1b597599fe4772ab690b36a95818), [`d14c463`](https://github.com/Effect-TS/effect/commit/d14c4631ac82911a8ca0b6c24c14ecb95863ee6b), [`d473bd3`](https://github.com/Effect-TS/effect/commit/d473bd3ee966a3a0c245d0e3c752fd91368b8ff6), [`84864bc`](https://github.com/Effect-TS/effect/commit/84864bc30c9e92a1226f65bb78b0641a7e0acea2), [`e2ae724`](https://github.com/Effect-TS/effect/commit/e2ae72481e755a454f28ce45e90f43b857b7c6e5), [`f921ed3`](https://github.com/Effect-TS/effect/commit/f921ed305c63415dc468e94d9319cc3237119138), [`1df933d`](https://github.com/Effect-TS/effect/commit/1df933d1dee2e09a25f9c27214b31aa94fa9d020), [`829aff9`](https://github.com/Effect-TS/effect/commit/829aff97d71d7900b7765ba2f63e33874b898cdc), [`aa0aba3`](https://github.com/Effect-TS/effect/commit/aa0aba383382ca1b99fbe618dceba63c3a5a6a53), [`a71140f`](https://github.com/Effect-TS/effect/commit/a71140fbe79809aa5311a55b1c6908905dc2df26), [`0276a27`](https://github.com/Effect-TS/effect/commit/0276a27ddbb15d163daf56f059ca9f62c4fd9939), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`c86c999`](https://github.com/Effect-TS/effect/commit/c86c99939e1628813a2f92f0906cfbbe924504e8), [`975f758`](https://github.com/Effect-TS/effect/commit/975f7586f3dcda5743d6570a740013a9fbdce9a7), [`2a3a478`](https://github.com/Effect-TS/effect/commit/2a3a4783bcd2d4e91e3505c7a6c1e398fea89595), [`1e6e206`](https://github.com/Effect-TS/effect/commit/1e6e206f6327a114f55a541775f465c0bc9fbf3e), [`fa6027b`](https://github.com/Effect-TS/effect/commit/fa6027b16e65376078e526fa33116ac0cc82485b), [`7616f73`](https://github.com/Effect-TS/effect/commit/7616f73c16bd45a3a3e2202d3f81c6d1cc63f4e0), [`fc668b6`](https://github.com/Effect-TS/effect/commit/fc668b61e9755545a47582dee6c4425d5652cbb6), [`d425c8c`](https://github.com/Effect-TS/effect/commit/d425c8c1319519e861714bcb9d055b63a9434fad), [`ce120f4`](https://github.com/Effect-TS/effect/commit/ce120f474b87d8a5a787dd7f848177ecd3b960fc), [`248201f`](https://github.com/Effect-TS/effect/commit/248201f7846d855685a4dc1b2f01968981706d69), [`e80d397`](https://github.com/Effect-TS/effect/commit/e80d3972fcca4e986b6497d67406f45ee1be540d), [`f1a941d`](https://github.com/Effect-TS/effect/commit/f1a941d52c2f71973e998883869e4e9ef510abf2), [`73bc3a1`](https://github.com/Effect-TS/effect/commit/73bc3a13b6e1b7a060e6bcd19ae7796443211a80), [`b628bb1`](https://github.com/Effect-TS/effect/commit/b628bb1240a09df2244c3ff6c587f978feb25225), [`6e3ae7b`](https://github.com/Effect-TS/effect/commit/6e3ae7b6359bda08cea74b761fd54224d7af45e2), [`e891247`](https://github.com/Effect-TS/effect/commit/e8912473bd4128714a0f8cb04621f3e12f62a910), [`53e6c73`](https://github.com/Effect-TS/effect/commit/53e6c7311b948a62e93e57f6daee66a8ec24b030), [`1579d6f`](https://github.com/Effect-TS/effect/commit/1579d6fd063e444e4fcdfd08ff194c7485203f9c), [`d3c6b73`](https://github.com/Effect-TS/effect/commit/d3c6b73cc4b78f22fdb2e25c8bebda027551540b), [`4a59c6a`](https://github.com/Effect-TS/effect/commit/4a59c6a559e69d399b00c6acd48c8e1977e34875), [`145d8e1`](https://github.com/Effect-TS/effect/commit/145d8e1013220425b8edf34f7011c73f73e1cdcf), [`f74282c`](https://github.com/Effect-TS/effect/commit/f74282cb08de808b678ce67687acbcd61939d2f6), [`0a38623`](https://github.com/Effect-TS/effect/commit/0a3862349ebfbc41746aa0de5e682992719c1da6), [`0a08ae0`](https://github.com/Effect-TS/effect/commit/0a08ae0626f8124779ec2e32ae2088d28db157c6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`d60c5d4`](https://github.com/Effect-TS/effect/commit/d60c5d40b5954ea1557aef1502b4b87f98bbf134), [`07ffd25`](https://github.com/Effect-TS/effect/commit/07ffd25bd0e82c9e6b72b8e669833fb4531a9da3), [`9b517ad`](https://github.com/Effect-TS/effect/commit/9b517ad28a0c2f213693bcab408658c40e3b6d9b), [`604b1c1`](https://github.com/Effect-TS/effect/commit/604b1c109d6c9e654b9b7fcc98d6b80361400325), [`ccc2e02`](https://github.com/Effect-TS/effect/commit/ccc2e02def5965f5535ca97fac2051f4387c4e13), [`14d810a`](https://github.com/Effect-TS/effect/commit/14d810a726caad8a7b0d42f04a6876f25fbcc005), [`a9d1ee3`](https://github.com/Effect-TS/effect/commit/a9d1ee3d4d51e97ea33440fe6100935d5fd5aada), [`a31adbe`](https://github.com/Effect-TS/effect/commit/a31adbefd6c04c70536de11433edba9edd1f5013), [`7245f87`](https://github.com/Effect-TS/effect/commit/7245f87ace09806d15893df8f0bac8f0b9b817db), [`3a0828b`](https://github.com/Effect-TS/effect/commit/3a0828b058abb4b4646bd880458b4e636d77d1c3), [`cd83544`](https://github.com/Effect-TS/effect/commit/cd83544dd594b99e8ea0d7a7645c086f5861769b), [`6550a07`](https://github.com/Effect-TS/effect/commit/6550a074c5c501d8a95dcd354f17e9785a1ae1eb), [`6f090d4`](https://github.com/Effect-TS/effect/commit/6f090d4c2032cc35f7f9a9eac900d0329b8ac07e), [`b505c0d`](https://github.com/Effect-TS/effect/commit/b505c0db03e86eb126237690a5867f457eb4237f), [`186dd49`](https://github.com/Effect-TS/effect/commit/186dd4914084fa346a870ce9c637b9c2a6cc8100), [`9f37e58`](https://github.com/Effect-TS/effect/commit/9f37e58d00e06150d96af48fffe780947ca0d519), [`4446451`](https://github.com/Effect-TS/effect/commit/44464510114773e1ebd1abb6efffa382cf69998e), [`58be972`](https://github.com/Effect-TS/effect/commit/58be97238ee1f6cd26f6a3a645d1df97a091a224), [`1320075`](https://github.com/Effect-TS/effect/commit/1320075fd4ed381f60f6f37f280dbec17cb29b81), [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80), [`f7490d4`](https://github.com/Effect-TS/effect/commit/f7490d49c75b99118c6c42549511ffa166459f59), [`c8ea602`](https://github.com/Effect-TS/effect/commit/c8ea60231ad69e23c0bf319f6f6b3ff28fc45e4b), [`62d82f4`](https://github.com/Effect-TS/effect/commit/62d82f4919c72d8cc0cfd2b704ed6900f69ad9a8), [`a2c9e7c`](https://github.com/Effect-TS/effect/commit/a2c9e7c17a4118b74a1eab3332042dafe0bb7318), [`97dd022`](https://github.com/Effect-TS/effect/commit/97dd022fe7fbd1696dd60f3319eae594abdd3760), [`f984ee8`](https://github.com/Effect-TS/effect/commit/f984ee82d255c7958eeed6f0dd08de1b88cbbfd6), [`f1b2910`](https://github.com/Effect-TS/effect/commit/f1b291027c3055f801c2f288dda21ba84c361956), [`a29e05a`](https://github.com/Effect-TS/effect/commit/a29e05a907053a8617763b4e7408e1f012b59049), [`1aa1d8b`](https://github.com/Effect-TS/effect/commit/1aa1d8bd0f2c2d429938657927f34f8ff0f940a0), [`bb99734`](https://github.com/Effect-TS/effect/commit/bb99734ef87bcc377c628bc4072aa856ce5f7ca6), [`222e7ca`](https://github.com/Effect-TS/effect/commit/222e7caa4bb0da6d81a4eabf25dffc181c20f6a3), [`797c9e3`](https://github.com/Effect-TS/effect/commit/797c9e3c1f2e66db1570cf6aa5c4a15e94507bf0), [`b4d5398`](https://github.com/Effect-TS/effect/commit/b4d5398598c04a84054a55873c943d587880058d), [`c8349ed`](https://github.com/Effect-TS/effect/commit/c8349ede1a2add807e9ec9c37612b3a39bf284f8), [`8426e5f`](https://github.com/Effect-TS/effect/commit/8426e5f7ad11f15e39565cf8ba0874bf974f5085), [`26e0085`](https://github.com/Effect-TS/effect/commit/26e0085d098f6e526c085c357b8d82ee068b6518), [`1a86166`](https://github.com/Effect-TS/effect/commit/1a86166f102b517ee4d89dca0095e28f738eee10), [`0856631`](https://github.com/Effect-TS/effect/commit/0856631a89d8f72d206c366905ddac75468a8e85), [`0af0985`](https://github.com/Effect-TS/effect/commit/0af0985d5c132b33e14a2df59e0567d7ba2a1710), [`bc582c9`](https://github.com/Effect-TS/effect/commit/bc582c94313cbb89d87b3844ffffabebbb4ffa83), [`2c63f1e`](https://github.com/Effect-TS/effect/commit/2c63f1e07e375b7958e14929c6a4b358c819a9d0), [`0d98213`](https://github.com/Effect-TS/effect/commit/0d9821346453d44696258103643d5482e63638b4), [`ca6f0dc`](https://github.com/Effect-TS/effect/commit/ca6f0dc4fcea564b351bf505df015d16a8b965ed), [`d8cc9ed`](https://github.com/Effect-TS/effect/commit/d8cc9ed80e1df9c056a46ba62df01f7fc0f531c3), [`42fd969`](https://github.com/Effect-TS/effect/commit/42fd969b45025eed6d60cb87b97c5ff88d187671), [`5641ad3`](https://github.com/Effect-TS/effect/commit/5641ad333a88bb9e56baf886006682678391be0a), [`629870d`](https://github.com/Effect-TS/effect/commit/629870d461b36d3dacb30c8d203b8d20d4beaba0), [`d592c14`](https://github.com/Effect-TS/effect/commit/d592c1492ad895ea72537e10387b735f3ba0e7de), [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb), [`53909a9`](https://github.com/Effect-TS/effect/commit/53909a9bf14b29725a08dcccde02bee8e95a6b14), [`78a4269`](https://github.com/Effect-TS/effect/commit/78a42697f6085c87c09b18e8095bb7541bd65261), [`06c6307`](https://github.com/Effect-TS/effect/commit/06c6307d3d5538bc398c67a44239bc3d52ab1a4d), [`0847c41`](https://github.com/Effect-TS/effect/commit/0847c41e2472a347cd8f77d460d3cbdfecd4adf0), [`5a77084`](https://github.com/Effect-TS/effect/commit/5a77084651bc58617b9988606473cf9ba5ca0032), [`96f99b3`](https://github.com/Effect-TS/effect/commit/96f99b325da32bfa535e826d89d838d0edec0dea), [`7bb8781`](https://github.com/Effect-TS/effect/commit/7bb878176b8ad20cf0e671b570caf72e194f9515), [`4907e9b`](https://github.com/Effect-TS/effect/commit/4907e9b2470894ddaf0cbdec2612b59f04a51dbe), [`2a30248`](https://github.com/Effect-TS/effect/commit/2a30248b6eb739f22403456209bc468f2f4ef26a), [`8364ddd`](https://github.com/Effect-TS/effect/commit/8364ddda233d35ee472cc2356312317a60a148a2), [`ad67d8c`](https://github.com/Effect-TS/effect/commit/ad67d8c5a1b195ff5468a9f2e963c59aa0b3166f), [`ec0c087`](https://github.com/Effect-TS/effect/commit/ec0c0875262fb81c7992f5023776b8fa894c49b6), [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72), [`a2c1ce6`](https://github.com/Effect-TS/effect/commit/a2c1ce62f04214d98d848f4cfe33d677e1a87d39), [`91e9af0`](https://github.com/Effect-TS/effect/commit/91e9af0d5f138933d3a131ca1cfbc52d3bc55481), [`7bd3f34`](https://github.com/Effect-TS/effect/commit/7bd3f34385a06157b249a351e32ffdcbe1cf2c37), [`ef16581`](https://github.com/Effect-TS/effect/commit/ef165814eae0a199fdca5b3c2839bb4fcdfd5b5d), [`1693a87`](https://github.com/Effect-TS/effect/commit/1693a877324273f5a23fa7d7fc892f4ee12f20c2), [`df3fc47`](https://github.com/Effect-TS/effect/commit/df3fc47609eda7843dae2b557c8fe834299e26b4), [`e11be41`](https://github.com/Effect-TS/effect/commit/e11be41dfc73fa6b5643ab9af7df18d6907927d7), [`2e39e8b`](https://github.com/Effect-TS/effect/commit/2e39e8bafef7d3fee9f3b324b7d50cc064376667), [`11c5ee7`](https://github.com/Effect-TS/effect/commit/11c5ee7202acd3bb789d0a8ce78e6c4523071553), [`1742d2f`](https://github.com/Effect-TS/effect/commit/1742d2f48441cb7b1a8d9aca3a8e6f0acbc35a96), [`8efc70e`](https://github.com/Effect-TS/effect/commit/8efc70ea16bee43fd9adde2871cf82a8131b80a0), [`9642776`](https://github.com/Effect-TS/effect/commit/964277661423f398f777ec2eab4b3ecc0b53f53b), [`6680828`](https://github.com/Effect-TS/effect/commit/668082885f52672e4c7fad4e0fe2bfa0e69703e2), [`c34edcb`](https://github.com/Effect-TS/effect/commit/c34edcb6d7a6151d53e19828c92abf7dd8450c2a), [`7b2c5bd`](https://github.com/Effect-TS/effect/commit/7b2c5bd3c394fa4751fb229394ac9c03e401f441), [`1a2ccee`](https://github.com/Effect-TS/effect/commit/1a2ccee2bbb93514dc66e0a9cdeb52a82172ab0e), [`db995df`](https://github.com/Effect-TS/effect/commit/db995df19b0381b64cc39b50aa28d7bd8f2c1294), [`af0ccdd`](https://github.com/Effect-TS/effect/commit/af0ccdd13aa8917bce7161b8bc257fbe51c6a6a5), [`addeaea`](https://github.com/Effect-TS/effect/commit/addeaea0b0abe0dc24ca1d4d0fd194a0e458be70), [`7704034`](https://github.com/Effect-TS/effect/commit/770403411bcd8befef0552a0e40066abc417f6ea), [`e72b12f`](https://github.com/Effect-TS/effect/commit/e72b12fc305710550bc6dcb978e92de8abff88cd), [`310dd9c`](https://github.com/Effect-TS/effect/commit/310dd9ce9681e97f558bb042adb35f6040ab81e3), [`1c2afc1`](https://github.com/Effect-TS/effect/commit/1c2afc15bbf4ce5a91d24da99bc275b5bd744e77), [`44f44ca`](https://github.com/Effect-TS/effect/commit/44f44cae65249801c082c3a3848016eeeac5eb24), [`f43b9d6`](https://github.com/Effect-TS/effect/commit/f43b9d6d99cb591c0377bb1474121827201627c9), [`56e72b3`](https://github.com/Effect-TS/effect/commit/56e72b38c01bbe6ec921ad151f83b7849444f219), [`50ef80e`](https://github.com/Effect-TS/effect/commit/50ef80e251b85b5ee63aed06fe0244b5433ec2fa), [`fc3b718`](https://github.com/Effect-TS/effect/commit/fc3b7187114a31cbb21a250f55fda601bb3edcf0), [`ee336d8`](https://github.com/Effect-TS/effect/commit/ee336d80b4024b51654e7a2b8d869d244153fcdf), [`d12f922`](https://github.com/Effect-TS/effect/commit/d12f922853eff2310dbe9fc9b31562ad81b38435), [`46d8310`](https://github.com/Effect-TS/effect/commit/46d83101e8408c0428d9a8760e06e23a69ad6070), [`59812fd`](https://github.com/Effect-TS/effect/commit/59812fd221cde16fc77c03d326c6a2cf8fbec6b9), [`f9d0dec`](https://github.com/Effect-TS/effect/commit/f9d0dec12c5f229f05d7a74d532a85c6bbffefbf), [`4372c79`](https://github.com/Effect-TS/effect/commit/4372c79a32fcecc7b6b284d49a215b2c4fef9d77), [`81485ef`](https://github.com/Effect-TS/effect/commit/81485ef0288a3321e75d7d4ae32b7e0e06b6f86b), [`bd393d6`](https://github.com/Effect-TS/effect/commit/bd393d63c19bdd0ab212d95576cec89051c8501c), [`0c95c04`](https://github.com/Effect-TS/effect/commit/0c95c04b41c2d5d09e4564fd997572207fd72595)]: + - effect@4.0.0-rc.113 + ## 4.0.0-rc.112 ### Patch Changes diff --git a/repos/effect/packages/atom/vue/package.json b/repos/effect/packages/atom/vue/package.json index 97d57eaab..36747dab5 100644 --- a/repos/effect/packages/atom/vue/package.json +++ b/repos/effect/packages/atom/vue/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-vue", - "version": "4.0.0-rc.112", + "version": "4.0.0-rc.117", "type": "module", "license": "MIT", "description": "Vue bindings for the Effect Atom modules", @@ -61,7 +61,7 @@ }, "devDependencies": { "effect": "workspace:^", - "vue": "^3.5.42" + "vue": "^3.5.43" }, "peerDependencies": { "effect": "workspace:^", diff --git a/repos/effect/packages/atom/vue/src/index.ts b/repos/effect/packages/atom/vue/src/index.ts index a2f434007..b4fff5a91 100644 --- a/repos/effect/packages/atom/vue/src/index.ts +++ b/repos/effect/packages/atom/vue/src/index.ts @@ -206,6 +206,7 @@ export const useAtomRef =
(atomRef: () => AtomRef.ReadonlyRef): Readonly { value.value = next })) + value.value = ref.value }) return value as Readonly> } diff --git a/repos/effect/packages/atom/vue/test/index.test.ts b/repos/effect/packages/atom/vue/test/index.test.ts index 79cf1df55..ae0b2f885 100644 --- a/repos/effect/packages/atom/vue/test/index.test.ts +++ b/repos/effect/packages/atom/vue/test/index.test.ts @@ -1,5 +1,24 @@ -import { describe, test } from "vitest" +import { useAtomRef } from "@effect/atom-vue" +import { assert, describe, it } from "@effect/vitest" +import * as AtomRef from "effect/unstable/reactivity/AtomRef" +import { effectScope, nextTick, shallowRef } from "vue" describe("atom-vue", () => { - test("", () => {}) + describe("useAtomRef", () => { + it("publishes the current value when the selected ref changes", async () => { + const first = AtomRef.make(1) + const second = AtomRef.make(10) + const selected = shallowRef(first) + const scope = effectScope() + try { + const value = scope.run(() => useAtomRef(() => selected.value))! + + selected.value = second + await nextTick() + assert.strictEqual(value.value, 10) + } finally { + scope.stop() + } + }) + }) }) diff --git a/repos/effect/packages/effect/ARBITRARY-FOLLOW-UPS.md b/repos/effect/packages/effect/ARBITRARY-FOLLOW-UPS.md deleted file mode 100644 index 862a47e3a..000000000 --- a/repos/effect/packages/effect/ARBITRARY-FOLLOW-UPS.md +++ /dev/null @@ -1,62 +0,0 @@ -# Native Arbitrary Follow-ups - -This file tracks unfinished work for the native Schema-first Arbitrary implementation. Settled behavior and technical -decisions belong in [ARBITRARY.md](ARBITRARY.md); migration guidance belongs in -[ARBITRARY-MIGRATION.md](ARBITRARY-MIGRATION.md). Remove an item from this file when it is resolved rather than keeping -completed implementation history here. - -Every production change must preserve the existing guarantees: - -- Schema remains the only catalog of primitive and structural generator constructors; -- discarded roots are bounded by `maxDiscards`; -- inspected shrink candidates, including rejected nodes, are bounded by `maxShrinks`; -- shrinking and replay remain deterministic for supported pure callbacks; -- recursive and mutually recursive Schemas retain their productivity guarantees; -- `Sample`, the shrink carrier, the PRNG, generation budgets, and compiler metadata remain private; -- runtime performance and bundle cost are measured before and after the change. - -## Conditional research - -These items are intentionally dormant until their trigger is observed. - -### Finite-domain metadata - -Evaluate private finite-domain metadata only if constructive unique generation demonstrates a real exhaustion or -productivity problem. Do not add public cardinality vocabulary preemptively. - -### Decoded collection and Declaration profiling - -Profile collection generation Links and Declaration decoding only when a new runtime baseline identifies a regression. -ReadonlyMap and ReadonlySet are compiler-owned; Effect-specific HashMap, HashSet, and Chunk keep declaration-local -generation Links. - -### Schema-scoped distribution customization - -Revisit application-owned distribution overrides, including deterministic Faker integration, only after a concrete -use case establishes the required scope and bundle boundary. A future design must work for checked and nested Schemas, -must not overload filter or declaration annotations with a second contract, and must not require synthetic Schemas in -test integrations. Prefer a derivation-time override mechanism over executable metadata captured by production Schema -modules. - -### Trace-informed shrinking - -Compare the current `Sample` tree with private structural spans or trace-informed shrinking only when a reproducible -case shows poor shrunk output or excessive candidate traversal. - -### Concrete failing-input persistence - -Evaluate persistence and reuse of concrete failing inputs last. After `map` or `flatMap`, an Arbitrary may no longer -have a Schema or codec capable of serializing its output, while the existing opaque replay token remains persistable. - -## Verification policy - -For every activated item, use the narrowest representative validation and record exact commands and artifacts: - -- focused Arbitrary runtime tests and typetests when the public types change; -- package type checking and linting; -- seeded generation, shrinking, and replay characterization relevant to the slice; -- focused warm and cold runtime scenarios; -- focused Arbitrary bundle fixtures plus production Schema bundle sentinels. - -Do not update public JSDoc, [ARBITRARY.md](ARBITRARY.md), the migration guide, or the changeset until the corresponding -behavior has passed its semantic, runtime, and bundle gates. diff --git a/repos/effect/packages/effect/ARBITRARY-MIGRATION.md b/repos/effect/packages/effect/ARBITRARY-MIGRATION.md deleted file mode 100644 index c40815c2d..000000000 --- a/repos/effect/packages/effect/ARBITRARY-MIGRATION.md +++ /dev/null @@ -1,335 +0,0 @@ -# Migrating to Native Arbitrary - -This guide covers migration from the fast-check bridge published in `effect@4.0.0-rc.109` to the native, -Schema-first module at `effect/unstable/arbitrary`. - -The new module removes fast-check from the `effect` package. Applications may still install and use fast-check -directly, but Effect Schema generation and `@effect/vitest` property tests no longer depend on it. - -For the new API and its semantics, see [Arbitrary in Effect](ARBITRARY.md). - -## Import Changes - -The following APIs have been removed: - -- `effect/testing/FastCheck`; -- `Schema.toArbitrary`; -- `Schema.Arbitrary`; -- the legacy `Schema.Annotations.ToArbitrary` contract and declaration-level `toArbitrary` annotation; -- the legacy `arbitrary` filter annotation; -- raw fast-check arbitrary inputs and `fastCheck` options in `@effect/vitest`. - -Import the native module explicitly: - -```ts -import { Arbitrary } from "effect/unstable/arbitrary" -``` - -If other tests still use fast-check-specific APIs, add fast-check as a direct development dependency and import it -from `"fast-check"`. Do not import it through Effect. - -## Generating Samples - -Previously, `Schema.toArbitrary` returned a factory that needed the fast-check module: - -```ts -import { Schema } from "effect" -import { FastCheck } from "effect/testing" - -const Person = Schema.Struct({ - name: Schema.String, - age: Schema.Int -}) - -const personArbitrary = Schema.toArbitrary(Person)(FastCheck) -const samples = FastCheck.sample(personArbitrary, { numRuns: 20, seed: 42 }) -``` - -Now derive and sample through the Effect-native module: - -```ts -import { Effect, Schema } from "effect" -import { Arbitrary } from "effect/unstable/arbitrary" - -const Person = Schema.Struct({ - name: Schema.String, - age: Schema.Int -}) - -const personArbitrary = Arbitrary.schema(Person) -const samples = await Effect.runPromise( - Arbitrary.sampleEffect(personArbitrary, { count: 20, seed: 42 }) -) -``` - -`Arbitrary.sampleEffect` returns an `Effect` because sampling is interruptible, uses Effect `Random` when no seed is -provided, and reports bounded generation exhaustion as a typed `SampleError`. - -The generated values still use the decoded Schema `Type`. The sequence and distribution are not compatible with -fast-check, even when the same numeric seed is used. - -## Checking Properties - -Previously, fast-check owned both the property and the runner: - -```ts -import { Schema } from "effect" -import { FastCheck } from "effect/testing" - -const integer = Schema.toArbitrary(Schema.Int)(FastCheck) - -FastCheck.assert( - FastCheck.property(integer, (value) => Number.isInteger(value)), - { numRuns: 100, seed: 42 } -) -``` - -Now `Arbitrary.checkEffect` runs a pure or Effectful property and returns a structured result: - -```ts -import { Effect, Schema } from "effect" -import { Arbitrary } from "effect/unstable/arbitrary" - -const result = await Effect.runPromise( - Arbitrary.checkEffect( - Arbitrary.schema(Schema.Int), - (value) => Number.isInteger(value), - { runs: 100, seed: 42 } - ) -) -``` - -Unlike `FastCheck.assert`, `Arbitrary.checkEffect` does not throw for an ordinary falsification. Handle `Passed`, -`Falsified`, `Exhausted`, and `ReplayMismatch` explicitly, or use `@effect/vitest`, which converts non-passing results -into test failures. - -Typed failures from Effectful properties are preserved in `Falsified.failure`. Defects and interruption continue -through the returned Effect. - -## Option Mapping - -The most common options map as follows: - -| Previous fast-check option | Native option | Migration note | -| -------------------------- | -------------------- | ------------------------------------------------------------------ | -| `numRuns` | `count` or `runs` | Use `count` for `sampleEffect` and `runs` for `checkEffect`. | -| `seed` | `seed` | The type is compatible, but generated sequences are not. | -| `path` | `replay` | Existing fast-check paths cannot be converted. | -| `maxSkipsPerRun` | `maxDiscards` | Native uses one absolute discard budget, not a multiplier per run. | -| `examples` | No direct equivalent | Keep explicit regression cases as ordinary tests. | -| `endOnFailure` | `maxShrinks` | Use `maxShrinks: 0` to stop at the initial failure. | -| `interruptAfterTimeLimit` | Effect interruption | Apply an Effect or test timeout around the check. | -| `skipAllAfterTimeLimit` | No direct equivalent | Prefer explicit run and discard bounds. | -| `verbose` | No direct equivalent | Inspect `CheckResult` or use `@effect/vitest` failure output. | - -Review any less common fast-check runner option manually. The native API deliberately does not reproduce the complete -`fc.Parameters` surface. - -## Replay Migration - -Fast-check replay used a seed plus a shrink `path`. Native replay uses one opaque token returned by a `Falsified` -result: - -```ts -const replayed = Arbitrary.checkEffect(arbitrary, property, { - replay: previousFailure.replay -}) -``` - -There is no conversion from a fast-check seed and path to a native replay token. Re-run the property with the native -engine, then record the new token from its `Falsified` result. - -Replay tokens are intended for reproducing and diagnosing a current failure. Because the module is unstable, they are -not guaranteed to survive upgrades. Preserve important failing inputs as explicit regression tests. - -## Migrating Declaration Annotations - -The old `toArbitrary` annotation directly constructed a fast-check arbitrary and exposed fast-check recursion and -constraint details: - -```ts -import { Schema } from "effect" - -class UserId { - readonly value: number - constructor(value: number) { - this.value = value - } -} - -const UserIdSchema = Schema.instanceOf(UserId, { - toArbitrary: () => (fc) => fc.integer({ min: 1, max: 1_000_000 }).map((value) => new UserId(value)) -}) -``` - -The native `toCodecArbitrary` annotation describes a generatable representation as a Schema `Link`: - -```ts -import { Schema, SchemaTransformation } from "effect" - -class UserId { - readonly value: number - constructor(value: number) { - this.value = value - } -} - -const UserIdSchema = Schema.instanceOf(UserId, { - toCodecArbitrary: () => - Schema.link()( - Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000_000 })), - SchemaTransformation.transform({ - decode: (value) => new UserId(value), - encode: (id) => id.value - }) - ) -}) -``` - -Before adding `toCodecArbitrary`, check whether the declaration already has a useful `toCodecJson` or `toCodec`. -Native derivation falls back to those canonical codecs automatically. Add an arbitrary-specific Link only when the -canonical representation is opaque or generates valid values too rarely. - -The migration changes where generation logic lives: - -| Legacy contract | Native contract | -| ---------------------------------------------- | -------------------------------------------------------------- | -| Returns a `fast-check.Arbitrary` | Returns a `SchemaAST.Link` through `Schema.link`. | -| Receives generated arbitrary type parameters | Receives decoded Schema type parameters. | -| Receives fast-check constraints and recursion | Receives normalized constraints. | -| Manages terminal recursive branches explicitly | Leaves recursion analysis and budgets to the native compiler. | -| Uses arbitrary combinators | Uses Schema constructors, checks, and a Schema transformation. | - -The original declaration remains authoritative. Values decoded by the Link are checked against it. Failed decodes and -rejected values become bounded discards. - -### Custom Filter Metadata - -The old `arbitrary` filter annotation has been replaced by `arbitraryConstraint`. Ordinary custom filters continue to -work as residual filters without generation metadata: - -```ts -import { Schema } from "effect" - -const Even = Schema.Int.check( - Schema.makeFilter((value) => value % 2 === 0) -) -``` - -Residual filtering is bounded, so a very selective or impossible predicate may produce `SampleError` or `Exhausted`. - -If the previous annotation supplied a recognized constructive constraint, move it to `arbitraryConstraint` and adapt -its shape. The predicate remains authoritative: - -```ts -import { Order, Schema } from "effect" - -const Positive = Schema.Number.check( - Schema.makeFilter( - (value) => value > 0, - { - arbitraryConstraint: { - order: Order.Number, - minimum: 0, - exclusiveMinimum: true - } - } - ) -) -``` - -The main constraint-shape changes are: - -| Previous field | Native field | -| ------------------------------------ | ------------------------------------------------------------------- | -| `ordered.order` | `order` | -| `ordered.minimum` / `maximum` | `minimum` / `maximum` | -| `ordered.exclusiveMinimum` / maximum | `exclusiveMinimum: true` / `exclusiveMaximum: true` | -| `integer: true` | `number: "integer"` | -| `noNaN` and `noInfinity` | `number: "finite"` when both restrictions apply | -| collection `minLength` / `maxLength` | `minLength`, `minSize`, or `minProperties` and its matching maximum | -| string pattern | `{ source, flags }` in `patterns` | -| `unique: true` | `uniqueBy: identity` | -| `candidate` | No direct equivalent | - -Choose the cardinality field that matches the Schema domain: `minLength` and `maxLength` for strings and arrays, -`minSize` and `maxSize` for sized collections, and `minProperties` and `maxProperties` for object properties. - -For an opaque declaration that needs a reusable statistically better source domain, express that source as a Schema -Link with `toCodecArbitrary`. - -## Migrating `@effect/vitest` - -Property inputs may be Schemas, native Arbitraries, or mixtures of both. - -Schema-only properties need only an option rename: - -```ts -// Before -it.prop( - "commutative", - [Schema.Int, Schema.Int], - ([a, b]) => a + b === b + a, - { fastCheck: { numRuns: 200, seed: 42 } } -) - -// After -it.prop( - "commutative", - [Schema.Int, Schema.Int], - ([a, b]) => a + b === b + a, - { arbitrary: { runs: 200, seed: 42 } } -) -``` - -Raw or mixed fast-check inputs are no longer accepted: - -```ts -// No longer supported -it.prop("raw arbitrary", [fc.integer()], ([value]) => Number.isInteger(value)) -it.prop("mixed", [Schema.String, fc.integer()], ([text, value]) => true) -``` - -Replace those inputs with Schemas when they describe a domain supported by Schema, or compose a native Arbitrary: - -```ts -import { Arbitrary } from "effect/unstable/arbitrary" - -const integer = Arbitrary.schema(Schema.Int) - -it.prop("native arbitrary", [integer], ([value]) => Number.isInteger(value)) -it.prop("mixed", [Schema.String, integer], ([text, value]) => typeof text === "string" && Number.isInteger(value)) -``` - -If a test genuinely needs a fast-check-specific arbitrary or runner feature, use fast-check directly with Vitest -rather than passing it through `@effect/vitest`. - -`it.prop`, `it.effect.prop`, and `it.live.prop` all accept native check options under `arbitrary`. - -## Behavioral Differences to Review - -Migration is not only an import rename. Review the following differences: - -- native generation and shrinking have different distributions and may find different shrunk inputs; -- native checking returns structured results instead of using fast-check's assertion exceptions; -- generation that cannot find enough valid samples is bounded and reports `SampleError` or `Exhausted`; -- pure and Effectful properties share one interruptible runner; -- recursive and mutually recursive Schemas are analyzed as a graph and must have a finite generation path; -- replay tokens, seeds, and shrink paths are not compatible with fast-check; -- generated values are the decoded Schema `Type`; -- properties must not mutate generated values. - -## Migration Checklist - -1. Replace `effect/testing/FastCheck` imports. Use the native Arbitrary module for Schema generation and import - `"fast-check"` directly only where it is still independently required. -2. Replace `Schema.toArbitrary(schema)(FastCheck)` with `Arbitrary.schema(schema)`. -3. Replace `FastCheck.sample` with `Arbitrary.sampleEffect` and run the returned Effect. -4. Replace `FastCheck.check` or `FastCheck.assert` for Schema-derived inputs with `Arbitrary.checkEffect`, then handle its - structured result. -5. Rename `@effect/vitest` options from `fastCheck` to `arbitrary` and convert `numRuns` to `runs`. -6. Replace raw fast-check inputs in `@effect/vitest` with Schemas or native Arbitraries. -7. Migrate declaration-level `toArbitrary` callbacks to the `toCodecArbitrary` Link-returning contract and replace old - filter-level `arbitrary` annotations with `arbitraryConstraint`. -8. Re-run properties with the native engine and record new replay tokens or explicit regression examples. -9. Review discard limits for selective custom filters. diff --git a/repos/effect/packages/effect/ARBITRARY.md b/repos/effect/packages/effect/ARBITRARY.md index 9c7778024..aaba320f6 100644 --- a/repos/effect/packages/effect/ARBITRARY.md +++ b/repos/effect/packages/effect/ARBITRARY.md @@ -18,9 +18,6 @@ The API used here is currently available from `effect/unstable/arbitrary`. The ` the ideas are stable, but names, result types, generation policies, and replay format may still change before this module is promoted. -If you are upgrading from the earlier Schema arbitrary integration available in `effect@4.0.0-rc.109`, see the -[migration guide](ARBITRARY-MIGRATION.md). - ## Writing a First Property Consider the rule “adding zero does not change an integer.” With `@effect/vitest`, we can write it directly: @@ -165,6 +162,22 @@ match the input. After a failure, it simplifies one member at a time. Empty tupl Effect occasionally creates generated records without inherited `Object` methods. This can reveal code that assumes methods such as `hasOwnProperty` always exist. Prefer `Object.hasOwn(value, key)` when checking generated objects. +Use `array` for variable-length collections of an existing Arbitrary: + +```ts +const command = integers.pipe(Arbitrary.map((value) => ({ _tag: "Add" as const, value }))) +const commands = Arbitrary.array(command, { maxLength: 50 }) +``` + +`minLength` defaults to zero. Generation grows with `size`, honors explicit minima even at size zero, and respects +`maxLength`. Invalid bounds throw immediately. After a failure, `array` tries removing blocks, including prefixes and +interior blocks, while retaining the other elements in their original order. Retained objects keep their identity. +It also simplifies individual elements. The bounded search may stop before finding the smallest possible failure. + +This differs from generating a length and using `flatMap` with `all`: shrinking that length regenerates the dependent +contents. Use `array` when shrinking should remove commands from the failing sequence. `Arbitrary.schema` applied to +`Schema.Array` uses the same block-removal strategy; prefer it when the complete collection is already a Schema. + Use `flatMap` when one generated value decides what can be generated next. Create the possible generators before the callback when you can, because creating a Schema inside the callback repeats that work each time it runs: @@ -977,7 +990,7 @@ Effect finalizers still run. A timeout cannot stop a synchronous JavaScript call The module starts from Schema and currently does not provide: -- separate Arbitrary constructors for strings, numbers, arrays, and objects; describe those inputs with Schema; +- a second catalog of Schema-like constructors for strings, numbers, and objects; describe those inputs with Schema; - a public low-level generator constructor or direct access to simplification steps; - weighted choice; use [targeted scenarios](#targeting-rare-scenarios) to guarantee that a test reaches an important situation; @@ -1045,6 +1058,11 @@ The module therefore exposes `schema` and a small set of operations for combinin whose structure is hidden can provide a simpler Schema through `toCodecArbitrary`; it does not need a second language for generators. +`Arbitrary.array(item, options)` composes an existing element Arbitrary, including one built with `map`, `filter`, +`flatMap`, or `all`. It supports deleting commands while retaining the failing sequence's remaining values, which a +dependent length generator cannot preserve. Schema remains the place to describe primitive values and checked data +structures. + The implementation stored inside `Arbitrary` remains private. Users can combine and run an Arbitrary without depending on how Effect currently generates or simplifies values. Reconsider this decision only if an important input cannot be described by Schema or built clearly from existing Arbitraries. @@ -1142,8 +1160,7 @@ generated sequences, probabilities, and intermediate simplifications may change. The unstable interface does not currently expose: -- separate Arbitrary constructors for primitive values and structures, or direct access to internal samples and - simplification steps; +- a second catalog of Schema-like constructors, or direct access to internal samples and simplification steps; - weighted distribution controls; - custom simplification that is not attached to a Schema; - support for generators from other libraries or a broad set of runner settings; @@ -1228,17 +1245,27 @@ can alter generated values, simplification, replay, performance, or bundle size to `Schema.Struct`, `Schema.Record`, `Schema.Json`, and record-shaped `all`, but not to arrays, tuples, declarations, or collection classes. - Integer and BigInt generation avoids favoring some values accidentally. It tries boundary values more often on some - runs. Number generation includes signed zero, very small values, infinities, and `NaN` when the Schema permits them. - Finite and integer checks exclude the values they promise to exclude. -- The magnitude of unbounded integers grows with `size`. Ordinary strings combine printable ASCII with a fixed set of - JavaScript edge cases. Regular-expression length is measured in UTF-16 code units, matching JavaScript strings. + runs. Unbounded BigInts select their bit length separately from their value, mixing ordinary magnitudes with much + wider values even at a small `size`. Number generation includes signed zero, very small values, infinities, and `NaN` + when the Schema permits them. Finite and integer checks exclude the values they promise to exclude. +- BigDecimal generation selects precision and decimal exponent independently, while ordered constraints are projected + to the selected scale. Its simplifications operate on the represented numeric value, so a fixed generated scale does + not dictate the final counterexample. Local simplification continues between passing and failing values with a finite + limit on added precision. Ordinary strings combine printable ASCII with a fixed set of JavaScript edge cases. + Regular-expression length is measured in UTF-16 code units, matching JavaScript strings. - Exact probabilities, the value produced by a particular seed, and the order of simplifications may change. Source code comments credit algorithms adapted from other property-testing and random-number implementations. #### Generating and Simplifying Data Structures -- Arrays first remove optional or repeated items and then simplify remaining items. Objects choose optional properties - without favoring earlier declarations. If the first choice is too large for the available recursion space, Effect +- Arrays try removing progressively smaller blocks at successive positions and simplifying individual items. Deletions + retain the other elements and their shrink history, so an earlier element candidate can be tried again after deletion + changes the property context. This history is lazy and local to a collection generation. Products and objects also + retain shared child histories, so exploring a filtered descendant cannot exhaust a sibling's shrink candidates. + Ordinary field shrinking continues from the field being simplified; structural deletions restart field traversal. + Tuple prefixes and fixed tails cannot be deleted; optional positions can only be removed from the end when the tuple + permits it. Whole-array minima apply to every deletion. Objects choose optional properties without favoring earlier + declarations. If the first choice is too large for the available recursion space, Effect uses the smallest choice that still satisfies the Schema instead of rejecting a Schema that can produce a value. Simplification removes optional properties, simplifies values, and then simplifies generated keys while keeping keys unique. diff --git a/repos/effect/packages/effect/CHANGELOG.md b/repos/effect/packages/effect/CHANGELOG.md index 39af4f04f..00ce9d8bc 100644 --- a/repos/effect/packages/effect/CHANGELOG.md +++ b/repos/effect/packages/effect/CHANGELOG.md @@ -1,5 +1,1051 @@ # effect +## 4.0.0-rc.117 + +### Patch Changes + +- [#8331](https://github.com/Effect-TS/effect/pull/8331) [`9953c92`](https://github.com/Effect-TS/effect/commit/9953c92b860061ec928098b121279087ee56e5e9) Thanks @tim-smart! - Bound local sends that wait for an unregistered cluster entity type. These sends now fail with an `Entity type ... not registered` defect at the shared runner registration deadline. Once that startup deadline has elapsed, sends to not-yet-registered dynamic entity types fail immediately. + +- [#8319](https://github.com/Effect-TS/effect/pull/8319) [`c6e8b20`](https://github.com/Effect-TS/effect/commit/c6e8b20c27b08e94549a9431c38e867aa014b724) Thanks @fubhy! - Extend `NetAddress.toCanonical` to accept internet addresses. IPv4-mapped IPv6 addresses become IPv4 while retaining the port. Other addresses retain their identity and IPv6 scope metadata. + +- [#8317](https://github.com/Effect-TS/effect/pull/8317) [`8d40572`](https://github.com/Effect-TS/effect/commit/8d40572de3762d51b142cfff30e6927ef21e37a3) Thanks @tim-smart! - Fix cluster workflows stalling after request resets. + +- [#8323](https://github.com/Effect-TS/effect/pull/8323) [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1) Thanks @fubhy! - Add validated multicast address refinements for IP and MAC addresses. + +- [#8310](https://github.com/Effect-TS/effect/pull/8310) [`3b155e3`](https://github.com/Effect-TS/effect/commit/3b155e3e24e42b603d48dff5d3280715944998f0) Thanks @williamrobertson13! - Fix memory leaks in `Pool.makeWithTTL` with the usage strategy by releasing retired resources and consumed acquisition errors from the TTL queue. + +- [#8326](https://github.com/Effect-TS/effect/pull/8326) [`895d944`](https://github.com/Effect-TS/effect/commit/895d94410f90c45546aec593730e5630f200ca1b) Thanks @lloydrichards! - Normalize identified MCP output schemas to object roots across protocol versions. + Support identified object schemas in MCP elicitation requests. + +- [#8318](https://github.com/Effect-TS/effect/pull/8318) [`705fb68`](https://github.com/Effect-TS/effect/commit/705fb6852ac24791352bd78889d57f07cbe949a9) Thanks @fubhy! - Add `Queue.shutdownUnsafe` to synchronously discard buffered messages and settle pending queue operations from callbacks. Both `Queue.shutdown` and `Queue.shutdownUnsafe` return `false` when the queue has already been shut down or completed; previously, `Queue.shutdown` always returned `true`. + +- [#8325](https://github.com/Effect-TS/effect/pull/8325) [`87912c0`](https://github.com/Effect-TS/effect/commit/87912c014132648694015a5636177b2ba2bdeb2a) Thanks @fubhy! - Add `NetAddress.formatNativeHost` and `NetAddress.formatMulticastInterface` with explicit platform and scope ID map parameters for native socket APIs. + +- [#8308](https://github.com/Effect-TS/effect/pull/8308) [`106af64`](https://github.com/Effect-TS/effect/commit/106af64cf7117dfad53d3b11b61bb3d17d63bfca) Thanks @tristanz! - Release savepoints after nested transactions succeed or successfully roll back in PostgreSQL, PGlite, MySQL, libSQL, and the Node, Bun, React Native, and WASM SQLite clients. This frees PostgreSQL transaction locks before the outer transaction completes. + + Custom SQL clients can opt in through the new `releaseSavepoint` option. Clients that omit it are unchanged. + +- [#8323](https://github.com/Effect-TS/effect/pull/8323) [`1b21e0d`](https://github.com/Effect-TS/effect/commit/1b21e0df6ab777d6a1664dfabdea88511f2d25d1) Thanks @fubhy! - Add generic branded NetAddress classifications and named validating schemas. + Singleton address constants retain their base-family types; guards expose + classifications on demand. Operations that derive new address bits return only + the IPv4 or IPv6 family until callers revalidate the classification. + +- [#8305](https://github.com/Effect-TS/effect/pull/8305) [`3d59ae6`](https://github.com/Effect-TS/effect/commit/3d59ae6d5f9ff3e52cb6ed4a9f325320580218d5) Thanks @tim-smart! - Make `criteria` optional for `Decision.probability`. When supplied, `criteria` still requires descriptions for both `false` and `true`. + + `Decision.Probability.criteria` is now optional, so consumers reading outcome descriptions (for example, `decision.criteria.true`) must first guard against `undefined`. + +- [#8329](https://github.com/Effect-TS/effect/pull/8329) [`89c9a39`](https://github.com/Effect-TS/effect/commit/89c9a398412f1f5e7f9c6f697a1badd982bcd741) Thanks @tim-smart! - Warn when conflicting cluster workflow definitions reuse a tag. + +## 4.0.0-rc.116 + +### Patch Changes + +- [#7908](https://github.com/Effect-TS/effect/pull/7908) [`c19c63f`](https://github.com/Effect-TS/effect/commit/c19c63fb710422aaf00b8d923188aa2e52a6776f) Thanks @gcanti! - Add experimental JIT and AOT schema compilers that work through the existing + `SchemaParser` APIs and share a decoder registry. Enable JIT globally with + `effect/unstable/schema/SchemaJITCompiler/enable`, selectively with + `SchemaJITCompiler.enable(ast)`, or install generated AOT decoders without + requiring dynamic function construction. The new + `effect/unstable/schema/SchemaAOTCompiler/Build` entrypoint discovers direct + Schema exports from explicit module loaders and writes a self-installing AOT + module through Effect's `FileSystem` and `Path` services. Compiled decoders can + provide optional synchronous `decode` and `make` operations; normal + `SchemaParser` calls consume them transparently and retain `decodeEffect` and + `makeEffect` as the detailed fallbacks. AOT targets declare the operations to + prepare, so generated modules contain only those operation families and use + the interpreter if an omitted operation is later called. + + ### Breaking changes + + `SchemaGetter.Getter` is now a tagged union that distinguishes synchronous, + optional, and effectful transformations. Getter values now expose only `pipe`. + Use the dual `SchemaGetter.map`, `SchemaGetter.compose`, and `SchemaGetter.run` + functions instead of the former methods. Keeping these operations standalone + lets bundlers remove composition code when an application does not use it. + + The public `Getter` constructor is removed. Use + `SchemaGetter.transformOptionalEffect` instead of `new SchemaGetter.Getter`. + `SchemaGetter.onSome` and `SchemaGetter.onNone` are also removed. Use + `transformEffect` for an effectful transformation of present values and + `transformOptionalEffect` when the transformation handles missing values. + + `Transformation#compose` is replaced by the dual standalone function + `SchemaTransformation.composeTransformation`. Replace `first.compose(second)` + with `SchemaTransformation.composeTransformation(first, second)` or + `SchemaTransformation.composeTransformation(second)(first)`. + + `SchemaTransformation.make` is renamed to + `SchemaTransformation.makeTransformation`. `SchemaTransformation.Transformation` + and `SchemaTransformation.Middleware` now implement `Pipeable`, so both values + can be passed through standalone combinators with `.pipe(...)`. + + `SchemaAST.Context.constructorDefault` now stores the constructor-default + `Effect` directly instead of wrapping it in a `SchemaAST.Link`. Constructor + defaults apply only during construction, so the direct representation avoids + giving them encoding semantics and lets construction reuse already-completed + synchronous Effects. Code that constructs or inspects `SchemaAST.Context` + should pass or read the default `Effect` directly. + +- [#8287](https://github.com/Effect-TS/effect/pull/8287) [`8cb0a4f`](https://github.com/Effect-TS/effect/commit/8cb0a4f28fba991e15659d08ecc09da28cf742d2) Thanks @tim-smart! - Add `Decision` and `DecisionModel` to `effect/unstable/ai` for batched classification, rating, and probability estimates over schema-encoded input. + +- [#8221](https://github.com/Effect-TS/effect/pull/8221) [`8f420bb`](https://github.com/Effect-TS/effect/commit/8f420bb3dc3c9be8c4a48d57dccee201dcb0260d) Thanks @gcanti! - Add `Arbitrary.array(item, { minLength, maxLength })` for variable-length arrays of custom Arbitraries. Shrinking removes blocks of commands while preserving the remaining values and their order, and also simplifies individual elements. + + Schema-derived arrays now also try removing prefixes and interior blocks. Shrinking composed values and Schema objects preserves child candidates that were previously lost when exploring another branch. + + Shrunk results and replay paths may change from earlier native releases. Re-run affected properties to obtain new replay tokens, and preserve important failing inputs as regression tests. + +- [#8233](https://github.com/Effect-TS/effect/pull/8233) [`1393080`](https://github.com/Effect-TS/effect/commit/1393080f1cc8f47d459119fcb759e2cc00fd7356) Thanks @gold-beyond! - Add `Crypto.randomULID` to generate ULIDs from the current `Clock` timestamp and cryptographically secure random bytes. + +- [#8256](https://github.com/Effect-TS/effect/pull/8256) [`ccae354`](https://github.com/Effect-TS/effect/commit/ccae35423188f58d7c3dec5db3e36ed4bf42bcdf) Thanks @tim-smart! - Align the `Effect` and `Stream` APIs and fix several Stream type signatures. + + - `Effect.orElseSucceed` now passes the error to the fallback function, matching `Stream.orElseSucceed`. + - `Effect.isEffect` narrows to `Effect` instead of `any`. + - `Stream.bind`, `Stream.bindEffect` and `Stream.let` allow re-binding an existing field and produce the same record type as their `Effect` counterparts. + - `Stream.Success`, `Stream.Error` and `Stream.Services` are unconstrained and distributive like the `Effect` versions. + - `Stream.partition` returns `[passes, fails]` and takes a `capacity` option, matching `Stream.partitionQueue` and `Stream.partitionEffect`. Its default capacity remains 16. + - `Stream.mapBoth` takes `onElement` / `onError`, matching `Stream.tapBoth`. + - `Stream.scan` and `Stream.scanEffect` take a lazy initial state. + - `Stream.catchTags` rejects unknown tag keys like `Effect.catchTags`. + - Fixed the data-first overloads of `Stream.runIntoPubSub` (error type was dropped), `Stream.cross` (swapped type parameter names) and `Stream.mapAccumArray` (`onHalt` return type). + - Added `Stream.as`, `Stream.tapDefect`, `Stream.tapErrorTag` and `Stream.unwrapReason`. + - Stream concurrency options use the `Types.Concurrency` alias, and JSDoc categories were consolidated across both modules. + +- [#8235](https://github.com/Effect-TS/effect/pull/8235) [`553c403`](https://github.com/Effect-TS/effect/commit/553c403f1d9199df738f73446dacd090da2e698b) Thanks @tim-smart! - Retain durable deferred completions received before a workflow owner's first local run so replay can complete while the deferred reply is still being persisted. + + Keep pending completions in a cache keyed weakly by cluster activation. Handler rebuilds retain completions, and overlapping activations cannot clear each other's results. Results can be collected once their activation scope becomes unreachable; closing a scope alone does not guarantee collection. + + Release RPC stream and queue consumers when their request write fiber is interrupted. + +- [#8243](https://github.com/Effect-TS/effect/pull/8243) [`45b5103`](https://github.com/Effect-TS/effect/commit/45b510352d42ac55718f9f5f43573975b323143f) Thanks @tim-smart! - Fix lost durable deferred wake-ups when a ClusterWorkflowEngine execution suspends before its run reply is persisted. Deferred completions now wait for the current run reply before resuming, so discarded executions can replay without relying on caller retries. + +- [#8254](https://github.com/Effect-TS/effect/pull/8254) [`77a5612`](https://github.com/Effect-TS/effect/commit/77a56120354d1d3f7341b117266f211143a3734a) Thanks @tim-smart! - Fix file response content types: honor the `contentType` option and preserve explicit headers, including MIME types set by `HttpStaticServer`. The default `HttpPlatform.layer` now infers missing content types from file extensions. + + Web file responses on the default, Node, and Deno platforms prefer explicit content types, then nonempty `File.type`, then the file extension. + + Removed the unused `contentLength` option from `HttpServerResponse.file`; lengths are calculated from the file and requested range. + +- [#8229](https://github.com/Effect-TS/effect/pull/8229) [`1076170`](https://github.com/Effect-TS/effect/commit/10761707b5cae0a66ef605abd1737ae59a18f5ac) Thanks @tim-smart! - Fix a deadlock in the memory workflow engine when a durable deferred is completed from a finalizer in the workflow awaiting it, including `DurableDeferred.into` inside `DurableDeferred.raceAll`. + +- [#8206](https://github.com/Effect-TS/effect/pull/8206) [`f110af1`](https://github.com/Effect-TS/effect/commit/f110af1ac5a54a7d62c2b96e35d4521a09f3fa06) Thanks @tim-smart! - Fix multipart file streams hanging on body read errors and preserve error causes when persisting files. + +- [#8284](https://github.com/Effect-TS/effect/pull/8284) [`0045152`](https://github.com/Effect-TS/effect/commit/0045152cdf796f20be5e690c69c40da611f813b8) Thanks @tim-smart! - `HttpServerResponse.toWeb` now keeps a raw `Response`'s headers and `Set-Cookie` values when the body is omitted (HEAD, or status 204, 205 or 304). Bodyless outer statuses keep the outer status and status text; HEAD keeps the raw `Response`'s. Outer cookies are appended to native `Set-Cookie` headers instead of replacing them. + +- [#8202](https://github.com/Effect-TS/effect/pull/8202) [`51d4a2f`](https://github.com/Effect-TS/effect/commit/51d4a2f08a5c7691dc876415bc9fc0ecf467e153) Thanks @xia-chao! - Fix `RcRef.make` to treat `idleTimeToLive: 0` like `Duration.zero` and `"0 millis"` instead of an omitted option. + +- [#8298](https://github.com/Effect-TS/effect/pull/8298) [`4d4c4e8`](https://github.com/Effect-TS/effect/commit/4d4c4e8a4436d49997a5a7234860ede651467537) Thanks @gcanti! - Fix `Schema.Redacted` to wrap the transformed result of its inner schema during decoding and encoding. + +- [#8227](https://github.com/Effect-TS/effect/pull/8227) [`ccfe152`](https://github.com/Effect-TS/effect/commit/ccfe152d11bed497f2d26aba8ef1a3613d0d6746) Thanks @tim-smart! - Release non-persisted, interruptible `RunnerServer` handlers and mailbox slots when callers disconnect. + + Preserve dynamic `WithTransaction` annotations during replay and re-delivery. + +- [#8261](https://github.com/Effect-TS/effect/pull/8261) [`d30a0c8`](https://github.com/Effect-TS/effect/commit/d30a0c880f8ffe06d7f0695c17f35910f1fcfbc9) Thanks @nikhilsnayak! - Add HTTP `QUERY` support to clients, routers, HttpApi endpoints, and AI request metadata. Configure CORS support through `allowedMethods`; defaults are unchanged. + + OpenAPI 3.1 output represents `QUERY` through `x-oai-additionalOperations`, which requires consumer support for that extension. The OpenAPI generator accepts the extension and the native OpenAPI 3.2 `query` field. + +- [#8255](https://github.com/Effect-TS/effect/pull/8255) [`84fe64a`](https://github.com/Effect-TS/effect/commit/84fe64a5fbfdecd23b66c207d0daa848d59dd825) Thanks @tim-smart! - Preserve literal suffixes such as `:wait` in `/operations/:id:wait` across `HttpApiClient`, `HttpApiBuilder`, and OpenAPI paths. + + Server routes without a params schema keep their existing matching for `RouteContext` consumers. Schemas whose keys cannot be enumerated keep the existing fallback. + +- [#8228](https://github.com/Effect-TS/effect/pull/8228) [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72) Thanks @lloydrichards! - Support prompt titles in `McpServer.prompt` and `McpServer.registerPrompt`. + +- [#8228](https://github.com/Effect-TS/effect/pull/8228) [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72) Thanks @lloydrichards! - Add an `instructions` option to MCP servers for initialization and discovery responses. + +- [#7265](https://github.com/Effect-TS/effect/pull/7265) [`a2c4154`](https://github.com/Effect-TS/effect/commit/a2c4154cf8bcbe455bd43bf7f3f12d9cbf38247c) Thanks @lloydrichards! - Add server support for MCP protocol version 2026-07-28 through 2026_07_28 + +- [#8272](https://github.com/Effect-TS/effect/pull/8272) [`23a58c0`](https://github.com/Effect-TS/effect/commit/23a58c020b25dde573ce523c2d157032676de0a3) Thanks @fubhy! - Add `NetAddress.formatHost` and `NetAddress.inetAddressFromHostString` for socket APIs that accept numeric hosts and ports separately, preserving IPv6 scope IDs. Add `NetAddress.scopeIdsFromInterfaces` to build a scope map for resolving named IPv6 zones from supplied network interface entries without performing operating-system lookups. + +- [#8293](https://github.com/Effect-TS/effect/pull/8293) [`feef90c`](https://github.com/Effect-TS/effect/commit/feef90ccbd86e5ab49b7e3cc3845591e23bdcb1b) Thanks @gcanti! - Treat `Never` as an uninhabited branch during `Arbitrary.schema` derivation. Schemas with another finite generation path, including optional properties, unions, and empty collections, no longer fail derivation. + +- [#8212](https://github.com/Effect-TS/effect/pull/8212) [`755e863`](https://github.com/Effect-TS/effect/commit/755e863a793e5621183e7992cb3f85d29030ad7b) Thanks @IMax153! - Restrict `ByteSize.Input` strings to canonical non-negative integers with recognized units, rejecting malformed literals at compile time. Parse external strings and fractional quantities with `ByteSize.fromString` or `ByteSize.fromStringUnsafe` before passing them to APIs accepting `ByteSize.Input`. + +- [#8228](https://github.com/Effect-TS/effect/pull/8228) [`49e4b37`](https://github.com/Effect-TS/effect/commit/49e4b37b831a573567e0b67d3ec4593403dc2e72) Thanks @lloydrichards! - Honor `Tool.Strict` in MCP input schemas and argument validation. Strict dynamic tools require Effect schemas; raw JSON Schema is rejected at registration. + + Support identified input schemas for non-strict tools. Invalid arguments use `InvalidParams` on protocols before 2025-11-25 and `isError: true` results on newer protocols. + + Distinguish validation failures from declared handler failures. Declared failures return `isError: true` without `structuredContent`: error mode uses `Error.message` or schema-encoded text, and return mode uses the encoded payload. Declared failures do not produce internal-error diagnostics. + + Log and report internal failures, including defects and encoding errors, while keeping client messages generic. + + Allow `Toolkit.handle` to accept `SchemaAST.ParseOptions` for parameter decoding. Expose the `Toolkit.FailureOrigin` cause annotation and shared `Tool.FailureOrigin` type, with the same origin available in `Tool.HandlerResult.failureOrigin` on returned failures. + +- [#8269](https://github.com/Effect-TS/effect/pull/8269) [`9ad9891`](https://github.com/Effect-TS/effect/commit/9ad9891e24058065bcd445772e005f8ce4b3e42f) Thanks @tim-smart! - Add `HttpApi.ParseOptions` to configure server and client codecs at the API, group, or endpoint level. + + `Sse.decodeSchema` and `ChannelSchema.decode` accept parse options, and `Schema.Cause` encodes reasons to their wire fields. + + SSE decoding omits absent IDs, including with default options. Use `Schema.optional(Schema.String)` instead of `Schema.UndefinedOr(Schema.String)` for IDs. With `onExcessProperty: "error"`, declare `event` (default: `"message"`) and any `id`, including inherited IDs. + + `HttpApiSchema.StreamSse` data-mode types and OpenAPI schemas now make `id` optional. + +- [#8296](https://github.com/Effect-TS/effect/pull/8296) [`0beded0`](https://github.com/Effect-TS/effect/commit/0beded04f5cffe8dd263f989c38a609e27f27fa5) Thanks @gcanti! - Improve Schema-derived BigInt and BigDecimal arbitraries with wider magnitude, precision, and exponent coverage and numeric boundary shrinking. + + Cover intermediate magnitudes up to large one-sided BigInt bounds, handle BigDecimal bounds with widely different scales, and refine decimal counterexamples between simple values. + +- [#8260](https://github.com/Effect-TS/effect/pull/8260) [`2940742`](https://github.com/Effect-TS/effect/commit/2940742c3f8529bc3b024b379904fbefd40ece55) Thanks @xia-chao! - Fix repeated `text`, `json`, `arrayBuffer`, and `urlParamsBody` reads in `HttpServerResponse.toClientResponse` for raw Web `Response` bodies, preserving the original response for serving. + +- [#8270](https://github.com/Effect-TS/effect/pull/8270) [`63c1566`](https://github.com/Effect-TS/effect/commit/63c15662f90ae970969de29761207c99ecdbf66b) Thanks @tim-smart! - Fix YAML indentless sequences, multiline scalar folding, and comments. Reject unsupported compact nested sequences (`- - value`) and document streams. + + Previously accepted values such as `description: Use when: deploy` and `description: Deploy:` now throw `SyntaxError`: YAML forbids colons followed by whitespace or end-of-value in plain scalars. Quote these values, for example `description: "Use when: deploy"`. + +## 4.0.0-rc.115 + +### Patch Changes + +- [#8196](https://github.com/Effect-TS/effect/pull/8196) [`657254b`](https://github.com/Effect-TS/effect/commit/657254b8218628b0116497d09aaf783cb88279f3) Thanks @gcanti! - Optimize schema initialization while preserving custom constructor options. + +- [#8190](https://github.com/Effect-TS/effect/pull/8190) [`f9ef0e9`](https://github.com/Effect-TS/effect/commit/f9ef0e970260b5de143392d00593e6c8b4614cff) Thanks @javascript-unsafe! - Omit response bodies for statuses 204, 205, and 304 in `HttpServerResponse.toWeb` and the Bun/Deno HTTP adapters, preventing invalid Web responses and hung requests. Cancel omitted raw `ReadableStream` bodies, and finalize request resources without starting omitted Effect streams. + +- [#8187](https://github.com/Effect-TS/effect/pull/8187) [`4f73f9e`](https://github.com/Effect-TS/effect/commit/4f73f9e0bd9538260f0f7166e0e6abdbd507a0e8) Thanks @tim-smart! - Parameterize persistence lookup keys in both SQL backing stores' `getMany` queries. + +## 4.0.0-rc.114 + +### Patch Changes + +- [#8177](https://github.com/Effect-TS/effect/pull/8177) [`3ff4952`](https://github.com/Effect-TS/effect/commit/3ff49521afa6fe6c85c2c230f7e986f7374ef353) Thanks @tim-smart! - Allow `Effect.cachedWithTTL` to compute the TTL from each completed `Exit`, so successes and failures can use different cache durations. + +- [#8164](https://github.com/Effect-TS/effect/pull/8164) [`6d55555`](https://github.com/Effect-TS/effect/commit/6d55555b83701c42855758944985a6fdbf57fffe) Thanks @sam-goodwin! - Keep Node and Bun file stats usable when optional numeric metadata exceeds the safe integer range by returning `Option.none()` for those fields. + +- [#8162](https://github.com/Effect-TS/effect/pull/8162) [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3) Thanks @tim-smart! - Fix published declarations referencing symbols stripped as `@internal`, which broke consumers compiling with `skipLibCheck: false`. `Effectable.d.ts` now uses the public `Effect.TypeId`, `Match.d.ts` no longer aliases an internal `Contextual` type, `Schema.d.ts` ships the `AnnotationSchemaConstraint` alias it references, and the CLI's `toFlagDoc` helper is marked internal so it no longer leaks `Param.getParamMetadata`. + +- [#8160](https://github.com/Effect-TS/effect/pull/8160) [`d4e4ad5`](https://github.com/Effect-TS/effect/commit/d4e4ad57d06d0a30d5e8090ed951f080bcd3a3d7) Thanks @gcanti! - Fix `SchemaRepresentation.toCodeDocument` generating invalid TypeScript for optional tuple elements containing unions or nested readonly tuples. Optional element types are now parenthesized, for example `readonly [(string | number)?]` instead of `readonly [string | number?]`. Generated runtime schemas are unchanged. + +- [#8158](https://github.com/Effect-TS/effect/pull/8158) [`b1988f4`](https://github.com/Effect-TS/effect/commit/b1988f496eef07482f10f4cc03ef20b9570ea0ac) Thanks @gcanti! - Fix `SchemaRepresentation.toCodeDocument` dropping Struct fields named `__proto__` from generated schemas. These fields now use computed keys, such as `Schema.Struct({ ["__proto__"]: Schema.String })`, so the generated schema validates them correctly. + +- [#8169](https://github.com/Effect-TS/effect/pull/8169) [`482b7d7`](https://github.com/Effect-TS/effect/commit/482b7d7eb08ebe6bc57781414a5a9a12600b2c71) Thanks @gcanti! - Improve `SchemaRepresentation.fromJsonSchemaDocument` and `fromJsonSchemaMultiDocument`: + + - Import `{ not: {} }` as `Schema.Never` ([#8137](https://github.com/Effect-TS/effect/issues/8137)). + + - Import closed records with one `patternProperties` entry, `additionalProperties: false`, and no declared or required properties when `patterns: "apply"` is enabled. These were previously rejected. + + ```json + { + "type": "object", + "patternProperties": { "^a": { "type": "number" } }, + "additionalProperties": false + } + ``` + + ```ts + Schema.Record( + Schema.String.check(Schema.isPattern(/^a/)), + Schema.Finite + ) + ``` + + - Reject open patterned objects with `patterns: "apply"` instead of generating incompatible TypeScript index signatures. + + ```json + { + "type": "object", + "patternProperties": { "^a": { "type": "number" } }, + "additionalProperties": true + } + ``` + + Import now explains that the generated TypeScript index signatures would give incorrect types to unmatched keys, and reports the source path. The same applies when `additionalProperties` is omitted or `{}`. Patterns can still be combined with a closed object in `allOf` when the result has a finite set of keys. Use `patterns: "ignore"` only if you intend to discard the pattern and its value constraints. + + - Reject references inside a subschema with its own `$id` instead of potentially resolving against the wrong definitions. Resolve or flatten these references before importing. A `$id` on the document root remains supported. + + ```json + { + "$id": "https://example.com/root", + "$defs": { "Value": { "type": "string" } }, + "type": "object", + "properties": { + "child": { + "$id": "child", + "$defs": { "Value": { "type": "number" } }, + "$ref": "#/$defs/Value" + } + } + } + ``` + + Here `child` refers to the nested numeric `Value`, not the root string `Value`. Import now reports that references inside a subschema with its own `$id` are unsupported instead of incorrectly using the root definition. + + - Explain import failures using JSON Schema keyword names, the reason for rejection, and the source path. Reference errors distinguish missing definitions, unsupported reference formats, and circular aliases. Pattern errors explain how to opt in for trusted schemas or explicitly discard the constraints. + +- [#8162](https://github.com/Effect-TS/effect/pull/8162) [`716e0c0`](https://github.com/Effect-TS/effect/commit/716e0c00942b42d36631b3114b1deb9a4a944ce3) Thanks @tim-smart! - Rename `Schema.Annotations.ToArbitrary.Constraint` to `Schema.Annotations.ToArbitrary.FilterConstraint`. + + Code that refers to the previous type name should update its type annotations to use `FilterConstraint`. + +- [#8181](https://github.com/Effect-TS/effect/pull/8181) [`9941e6d`](https://github.com/Effect-TS/effect/commit/9941e6dbf800cfd3723bfb421306e04e36b5882d) Thanks @Ishkirat-Singh! - Make `message` optional for `Prompt.Select` and `Prompt.MultiSelect`. When omitted, prompts display only the choices and submission shows a tick followed by the selected titles. `Prompt.AutoComplete` still requires a message. + +## 4.0.0-rc.113 + +### Patch Changes + +- [#7738](https://github.com/Effect-TS/effect/pull/7738) [`49e3901`](https://github.com/Effect-TS/effect/commit/49e39010ff84acbde32c50d559a2f76426cf830e) Thanks @kitlangton! - Retain completed tool approval results in non-streaming responses so Chat records them and does not replay approved tools on later turns. + +- [#7483](https://github.com/Effect-TS/effect/pull/7483) [`b945ded`](https://github.com/Effect-TS/effect/commit/b945ded23aa9a0ad88bb55aa4089680866dccf92) Thanks @tim-smart! - Align runtime type IDs with their module paths. Effect markers now omit legacy grouping prefixes and the `unstable` path segment, while OpenTelemetry spans use the `OtelTracer` module path. Custom implementations that copy these marker strings must adopt the corrected IDs. + +- [#8014](https://github.com/Effect-TS/effect/pull/8014) [`d6422f4`](https://github.com/Effect-TS/effect/commit/d6422f4104062e3e176abe92ef0e444cd4cc6bab) Thanks @kitlangton! - Fix `Effect.all` to retain errors and required services from every branch of a union of record inputs. + +- [#8095](https://github.com/Effect-TS/effect/pull/8095) [`5a80204`](https://github.com/Effect-TS/effect/commit/5a802043984727b0c5a291af39d1b9bbfa8d7b8b) Thanks @gcanti! - Fix `Arbitrary.schema` to respect applicable index signatures when generating and shrinking object properties, including fixed fields in `Schema.StructWithRest` and overlapping records. + + Combine compatible string, number, and bigint constraints during generation so cases such as a `String` field constrained by a `NonEmptyString` record remain productive at size zero. Other intersections are validated and may exhaust the discard budget. + +- [#7796](https://github.com/Effect-TS/effect/pull/7796) [`53511ef`](https://github.com/Effect-TS/effect/commit/53511efcd1c7a05d34b4c9a19c19ed317ebc5d4c) Thanks @kitlangton! - Fix `Schema.ArrayEnsure` to preserve array-valued element branches and outer-array encoding cardinality. + +- [#8067](https://github.com/Effect-TS/effect/pull/8067) [`79ae49f`](https://github.com/Effect-TS/effect/commit/79ae49f19cab9a42c8f27ae8b104d5e39e6131fa) Thanks @purwasadr! - Fix `AtomRpc.query` returning `never` for RPCs whose middleware declares service `requires`. The return-type conditional now infers all six `Rpc` type parameters, matching `mutation` and every utility in `Rpc`. + +- [#7463](https://github.com/Effect-TS/effect/pull/7463) [`0d083ba`](https://github.com/Effect-TS/effect/commit/0d083ba26b2e1afec8d3e8d83db0d05683b6602b) Thanks @tim-smart! - Remove the `mime` runtime dependency. The new `effect/unstable/http/Mime` module provides top-level lookup functions + backed by a vendored standard MIME registry. + +- [#7477](https://github.com/Effect-TS/effect/pull/7477) [`be0f822`](https://github.com/Effect-TS/effect/commit/be0f8221e37abd52668567b60fc1be28e3ff3803) Thanks @candrewlee14! - Allow sockets to use browser, Bun, and Node WebSocket implementations without consumer casts. Platform constructors + now support typed opening-handshake headers where available. + +- [#7587](https://github.com/Effect-TS/effect/pull/7587) [`debe8fd`](https://github.com/Effect-TS/effect/commit/debe8fdc15d00d51cf707353365d7b877123eec6) Thanks @kitlangton! - Fix `Cache.invalidateWhen` and `ScopedCache.invalidateWhen` deleting a replacement entry while waiting for an earlier lookup. + +- [#7585](https://github.com/Effect-TS/effect/pull/7585) [`a8588f9`](https://github.com/Effect-TS/effect/commit/a8588f99a43dc9b85dacb14f7be5d3e1ed994d4f) Thanks @kitlangton! - Fix interruption of `Cache.refresh` for a missing key removing a newer value written by `Cache.set`. + +- [#7596](https://github.com/Effect-TS/effect/pull/7596) [`f17eb0a`](https://github.com/Effect-TS/effect/commit/f17eb0a6f669e09647c5ded5832ea6baac596659) Thanks @kitlangton! - Fix `Cache.refresh` and `ScopedCache.refresh` exceeding capacity when an existing key is evicted while its refresh is in progress. Publishing the refreshed entry now evicts older entries as needed, releasing their resources in `ScopedCache`. + +- [#7595](https://github.com/Effect-TS/effect/pull/7595) [`f30cbfe`](https://github.com/Effect-TS/effect/commit/f30cbfe53951128ad8124ae733f12423dbc78bd8) Thanks @kitlangton! - Fix `Cache.refresh` for an initially missing key deleting a newer cached value when the refresh completes with zero time to live. + +- [#7614](https://github.com/Effect-TS/effect/pull/7614) [`78cc9c0`](https://github.com/Effect-TS/effect/commit/78cc9c0d0f36b59f7034b2ace56a4a5c02b7a551) Thanks @kitlangton! - Prevent `Cache` from retaining synchronously interrupted lookups. + +- [#7563](https://github.com/Effect-TS/effect/pull/7563) [`ccbdbd5`](https://github.com/Effect-TS/effect/commit/ccbdbd51e7bdb114302701fbc03021868b71956a) Thanks @alvarosevilla95! - Respect custom HTTP header redaction when recording server span attributes. + +- [#7254](https://github.com/Effect-TS/effect/pull/7254) [`a63dcbf`](https://github.com/Effect-TS/effect/commit/a63dcbf04e5c3d8d934a41bc6122e9951b1cefa9) Thanks @gcanti! - Add the experimental Schema-first `effect/unstable/arbitrary/Arbitrary` module for native generation without + fast-check. `Arbitrary.schema` derives an opaque arbitrary from the decoded Schema `Type`, `Arbitrary.sampleEffect` + provides interruptible sampling with typed exhaustion, and `Arbitrary.checkEffect` returns structured property results. + The initial implementation supports bounded discards, shrinking, replay, and recursive and mutually recursive Schemas. + `SampleError` and `Exhausted` include the effective seed so discarded runs remain reproducible even when the caller did + not provide one. `Arbitrary.isArbitrary` identifies values through the module's nominal protocol. Numeric constraints + retain `NaN` when it is accepted by their supported `Order.Number` bounds. Union derivation validates `oneOf` + exclusivity and isolates lazy cross-member shrinking from unrelated random generation. Object derivation keeps + optional-property selection constructive when candidate fields have different recursive costs. + Struct, Record, JSON-object, and record-shaped `Arbitrary.all` outputs periodically use a null prototype as an edge + case, preserving that prototype throughout shrinking and replay without perturbing structural PRNG choices. The change + adds 0.01–0.03 KB gzip to representative Arbitrary fixtures and leaves production-only bundle sentinels unchanged. + + Add `Arbitrary.map`, `Arbitrary.flatMap`, `Arbitrary.filter`, `Arbitrary.filterMap`, and `Arbitrary.all` for composing + derived Arbitraries without exposing a second catalog of primitive constructors. Filtering remains bounded and + promotes valid shrink descendants through rejected nodes. `maxShrinks` bounds every inspected shrink candidate, + including candidates rejected before property evaluation, while retaining the best shrunk input found when the + budget is exhausted. `flatMap` provides deterministic dependent generation, source-first shrinking, post-source PRNG + checkpoints, and one shared residual recursion budget. `all` combines tuples, iterables, and records with a shared + budget, randomized internal generation order, stable output shape, and independent member shrinking. Arbitrary values + implement `Pipeable` for composition with data-last combinators. + + Add the experimental Schema `arbitraryConstraint` and `toCodecArbitrary` annotations and their + `Schema.Annotations.ToArbitrary` types. Declarations can provide a Schema Link optimized for generation, while filters + can contribute native semantic constraints. The callback receives decoded type parameters and normalized constraints. + The compiler owns efficient representations for common built-ins, including JSON, RegExp, URL, Date, byte arrays, + ReadonlyMap, and ReadonlySet. Effect-specific HashMap, HashSet, Chunk, Graph, BigDecimal, and date-time declarations keep + local generation Links, while declarations with productive canonical codecs require no arbitrary-specific annotation. + `Schema.isUniqueKey` provides key-based Map uniqueness for explicit array representations. + + The same ownership policy applies to formatter and equivalence derivation: implementations for common declarations + live in their compiler, while domain-specific and dynamically constructed declarations retain local annotations. + Declarations whose intrinsic `Equal` implementation already matches their Schema equivalence need no annotation or + compiler special case. This keeps unused common callbacks out of production Schema bundles. + + Against the previous layout, `schema-toArbitrary` decreases from 36.68 KB to 33.24 KB gzip and + `arbitrary-combinators` decreases from 37.16 KB to 33.70 KB. `schema-toFormatter` increases from 18.92 KB to 19.49 KB + and `schema-toEquivalence` increases from 19.05 KB to 19.39 KB because callers that explicitly derive these capabilities + now retain the common declaration handlers. Generic production fixtures remain unchanged; an equivalence-specific + production fixture using common declarations decreases from 20.75 KB to 20.48 KB, while declarations whose intrinsic + equality is sufficient decrease from 23.42 KB to 23.34 KB. An Arbitrary-specific production fixture using common + declarations decreases from 20.35 KB to 19.61 KB, while one using the locally annotated BigDecimal and date-time + declarations increases from 18.34 KB to 23.01 KB. + The complete 31-scenario native Arbitrary comparison reports no statistically classified runtime regression; the five + moved BigDecimal and date-time scenarios remain within measurement noise. + + Add `SchemaGetter.forbiddenEncoding`, a reusable getter for the encode side of decode-only Schema transformations. + + Remove the fast-check bridge from the `effect` package, including `Schema.toArbitrary` and + `effect/testing/FastCheck`. Replace the legacy `Schema.Annotations.ToArbitrary` callback contract with the native + Schema-first types. The `effect` package no longer depends on fast-check. + + Migrate `TestSchema.Asserts.verifyLosslessTransformation` and `TestSchema.Asserts.arbitrary().verifyGeneration` to the + native runner. Both methods now accept native check options directly, bound unsuccessful generation, and include the + shrunk input and replay token in property failures. + + Use the Arbitrary runner for all `@effect/vitest` property tests. Property inputs may combine Schemas and Arbitraries, + and are composed directly with `Arbitrary.all`; check options are available through `arbitrary`. Raw fast-check + arbitraries and the `fastCheck` options object are no longer supported. As with the previous fast-check adapter, thrown + exceptions, defects, and typed failures from a property are shrinkable falsifications; Effect interruption remains an + interruption. + + Optimize constructive regular-expression generation by caching feasible lengths on the compiled pattern, computing + sequence-suffix feasibility once, and precomputing character-class metadata. Seeded generation, shrinking, and replay + remain unchanged. + + Optimize `BigDecimal.Order` and `BigDecimal.Equivalence` with a shared hybrid comparator. Ordinary scale differences + use cached, bounded coefficient alignment, while large differences are compared without materializing their decimal + zeroes. `BigDecimal.make` now rejects scales that are not safe integers. + + Before its removal, the materialized fast-check bridge fixture + `schema-toArbitrary-materialized-fast-check.ts` measured 79.00 KB minified and gzipped. + + Representative runtime measurements against corresponding hand-written fast-check 4.9.0 arbitraries are shown below. + Values are median latency on Node 24.12.0 and Apple M3; lower is better. Both implementations validate the + same output domains, although their generation distributions are not identical. Native speedup is fast-check latency + divided by Native latency, so higher is better. + + | Scenario | fast-check | Native | Native speedup | + | ----------------------------------- | ---------: | ------: | -------------: | + | 32 recursive samples | 150 µs | 103 µs | 1.45x | + | 128 optional Struct samples | 244 µs | 86.0 µs | 2.84x | + | 128 constrained strings | 742 µs | 49.7 µs | 14.86x | + | RegExp derivation and first sample | 13.4 ms | 30.8 µs | 429.02x | + | 64 RegExp strings | 595 µs | 919 µs | 0.64x | + | RegExp failure and shrinking | 168 µs | 88.2 µs | 1.91x | + | 128 bounded numbers | 68.9 µs | 21.8 µs | 3.18x | + | 128 `Uint8Array` samples | 98.3 µs | 74.4 µs | 1.32x | + | 128 `BigDecimal` samples | 66.6 µs | 56.3 µs | 1.18x | + | 128 `DateTime.Utc` samples | 71.2 µs | 50.5 µs | 1.42x | + | 128 named time zones | 52.2 µs | 27.9 µs | 1.85x | + | 128 time zones | 63.7 µs | 33.8 µs | 1.89x | + | 128 zoned date-times | 130 µs | 112 µs | 1.16x | + | 32 samples through Schema filter | 65.9 µs | 49.4 µs | 1.33x | + | 32 unique arrays | 156 µs | 132 µs | 1.18x | + | 128 literal samples | 40.0 µs | 3.70 µs | 10.78x | + | 128 mapped samples | 59.0 µs | 14.1 µs | 4.21x | + | 128 samples through passing filter | 58.9 µs | 13.9 µs | 4.23x | + | 32 samples through selective filter | 66.1 µs | 42.9 µs | 1.54x | + | 128 `filterMap` samples | 75.7 µs | 31.5 µs | 2.40x | + | Filtered failure and shrinking | 12.7 µs | 7.71 µs | 1.66x | + | 128 `all` tuple samples | 43.5 µs | 18.5 µs | 2.35x | + | 128 `all` record samples | 81.0 µs | 30.4 µs | 2.66x | + | 128 dependent `flatMap` samples | 125 µs | 67.2 µs | 1.86x | + | `flatMap` failure and shrinking | 20.1 µs | 6.71 µs | 2.99x | + | Replay `flatMap` shrink path | 14.3 µs | 6.57 µs | 2.17x | + | Passing property, 100 runs | 42.3 µs | 27.1 µs | 1.56x | + | `TestSchema`, 100 generations | 44.5 µs | 35.9 µs | 1.24x | + | First failure plus one shrink | 8.77 µs | 1.30 µs | 6.75x | + | Replay recorded failure | 6.35 µs | 1.19 µs | 5.36x | + + Cold recursive derivation is not included because the native fixture constructs and compiles a Schema, while the + fast-check fixture constructs a hand-written arbitrary; it is not a like-for-like warm-generator comparison. + + Add a guide for the native module and a migration guide from the fast-check bridge published in `effect@4.0.0-rc.109`. + +- [#7822](https://github.com/Effect-TS/effect/pull/7822) [`b845b18`](https://github.com/Effect-TS/effect/commit/b845b186379106c17ed4b1149e2462cd0d2d77b5) Thanks @tim-smart! - Add `Stream.catchDefect` and `Channel.catchDefect` for recovering from defects without catching typed failures or interruptions. + +- [#7657](https://github.com/Effect-TS/effect/pull/7657) [`381b794`](https://github.com/Effect-TS/effect/commit/381b79464f8e0a520585fdcfdd3e95e0553d0b27) Thanks @kitlangton! - Remove `Channel.runDone`; use `Channel.runDrain` to consume all output and return the completion value. + +- [#7989](https://github.com/Effect-TS/effect/pull/7989) [`4ffcaf4`](https://github.com/Effect-TS/effect/commit/4ffcaf4c4a94d3d9d5715db91aa37d7f5d9ecd99) Thanks @kitlangton! - Preserve astral Unicode escapes and following arguments in `ChildProcess.make` and `ChildProcess.prefix` template literals. + +- [#8018](https://github.com/Effect-TS/effect/pull/8018) [`ba2fd82`](https://github.com/Effect-TS/effect/commit/ba2fd822e9618f30d0f430ae4eed2eaa4813e2e3) Thanks @tim-smart! - Wait for Node child process groups to exit during scoped release and `kill`. + + After signalling a process group, both operations now wait for its leader and descendants. Without `forceKillAfter`, the wait is limited to one second and never escalates. With `forceKillAfter`, the group receives `SIGKILL` at the deadline, followed by a final wait of up to one second. Native timers keep escalation working under a `TestClock`, and cleanup no longer depends on stdio closing. + + `exitCode` and `isRunning` remain tied to the leader's exit, and a leader that already exited successfully still leaves its group untouched. Process group checks count zombies, so cleanup may wait for the full bound under a non-reaping PID 1. + +- [#7617](https://github.com/Effect-TS/effect/pull/7617) [`02be94c`](https://github.com/Effect-TS/effect/commit/02be94ce78a3c9fc1e4c516d37d8e11e1c0eef2e) Thanks @kitlangton! - Fix `Chunk` concatenation to preserve sliced elements. + +- [#7453](https://github.com/Effect-TS/effect/pull/7453) [`115d8c2`](https://github.com/Effect-TS/effect/commit/115d8c22599640ece2fd6a10564925b1d79f8a8c) Thanks @gcanti! - Rename the built-in `Config` constructors to PascalCase and rename `Config.mapOrFail` to `Config.mapEffect`. `Config.Array` and `Config.Record` now construct configs directly, with overloads for pathless options or a path followed by options, while their specialized schemas and the other built-in schemas are kept internal. + + This is a breaking naming cleanup for the Effect 4 release candidate. It makes casing consistently identify typed config constructors, aligns effectful mapping with the rest of the library, and prevents implementation schemas from expanding the public `Config` interface. + +- [#8020](https://github.com/Effect-TS/effect/pull/8020) [`1452635`](https://github.com/Effect-TS/effect/commit/14526354f3d0288bd1d0a85c8d4aeee8abe2cd9b) Thanks @kitlangton! - Ensure `Effect.acquireUseRelease` releases an acquired resource and `Effect.useSpan` ends its span when the use callback throws before returning an effect. The thrown exception remains a defect, but no longer skips cleanup. + +- [#8087](https://github.com/Effect-TS/effect/pull/8087) [`77f85fe`](https://github.com/Effect-TS/effect/commit/77f85fe1613348f5c990016b49dc97e252576c82) Thanks @tim-smart! - use `new` instantiation for streams + +- [#7802](https://github.com/Effect-TS/effect/pull/7802) [`a3f2b31`](https://github.com/Effect-TS/effect/commit/a3f2b319343f28edb028270f7074066cea0ec429) Thanks @kitlangton! - Preserve flags and nested commands when completing a CLI subcommand through its alias. + +- [#7804](https://github.com/Effect-TS/effect/pull/7804) [`310f8d3`](https://github.com/Effect-TS/effect/commit/310f8d3424a025ce1f1cc5c338232db2362052d8) Thanks @kitlangton! - Include inherited shared flags in descendant CLI completions. + +- [#8086](https://github.com/Effect-TS/effect/pull/8086) [`291d616`](https://github.com/Effect-TS/effect/commit/291d616282b666bbba3664143fa1d19e839a6a5d) Thanks @MaxFreedomPollard! - Allow `=` in values parsed by `Primitive.keyValuePair`, `Flag.keyValuePair`, and `Param.keyValuePair` in `effect/unstable/cli`. + +- [#7687](https://github.com/Effect-TS/effect/pull/7687) [`48dbbb2`](https://github.com/Effect-TS/effect/commit/48dbbb22bb2e04f63992fc267dd711a0cbd76d72) Thanks @kitlangton! - Allow optional alternative CLI flags. + +- [#8121](https://github.com/Effect-TS/effect/pull/8121) [`b43bfd6`](https://github.com/Effect-TS/effect/commit/b43bfd644a3205f1518abbfd006f761e31b3a5f9) Thanks @tim-smart! - Rename CLI constructors to PascalCase, aligning scalar names with `Schema` and `Config`. This is a breaking change; parsing behavior is unchanged. + + In `Primitive`, `Param`, `Flag`, and `Argument`, capitalize existing constructor names, with these exceptions: + + | Previous | New | Modules | + | --------- | ---------- | --------------------- | + | `integer` | `Int` | All four | + | `float` | `Finite` | All four | + | `none` | `Never` | All four | + | `choice` | `Literals` | Param, Flag, Argument | + + `Primitive.choice` becomes `Primitive.Choice`; `choiceWithValue` becomes `ChoiceWithValue` where available. + + In `Prompt`, capitalize control constructors except `text` → `String`, `integer` → `Int`, and `float` → `Number`. Rename public types `IntegerOptions` → `IntOptions` and `FloatOptions` → `NumberOptions`. Shared `TextOptions` is unchanged. `Prompt.Number` retains its existing parser, without a finite-number restriction. + + In `GlobalFlag`, rename `action` → `Action` and `setting` → `Setting`. Factories and combinators, including `Command.make` and `Prompt.succeed`, keep their names. + + Update public `_tag` matches and completion descriptors: + + - `Primitive`: `"Integer"` → `"Int"`, `"Float"` → `"Finite"`, `"None"` → `"Never"`. + - `Completions.FlagType` and `Completions.ArgumentType`: `"Integer"` → `"Int"`, `"Float"` → `"Finite"`. + + Sentinels still always fail; their internal parameter name is now `"__never__"`. Help labels and completion scripts are unchanged. + +- [#7685](https://github.com/Effect-TS/effect/pull/7685) [`1c89c78`](https://github.com/Effect-TS/effect/commit/1c89c78f9faa3638f398772f3ed7182aa97a1edd) Thanks @kitlangton! - Fix defaulted variadic arguments when omitted. + +- [#8059](https://github.com/Effect-TS/effect/pull/8059) [`9bbe1a5`](https://github.com/Effect-TS/effect/commit/9bbe1a5940eee4642ffe854356128b617e0a064c) Thanks @kitlangton! - Fix CLI wizard handling of negative numbers and other flag values beginning with `-`. + +- [#7489](https://github.com/Effect-TS/effect/pull/7489) [`dd99ab0`](https://github.com/Effect-TS/effect/commit/dd99ab007e3352761187dae330d52f65feeff7c0) Thanks @tim-smart! - Cluster no longer retains fiber ids for every local teardown. + + Transient persisted interrupts are now classified from live teardown state + (entity, shard, singleton, entity type, and node shutdown) instead of a + process-lifetime set of fiber ids. The registry is bounded by in-flight + teardowns and returns to baseline after entity reap storms. + +- [#7939](https://github.com/Effect-TS/effect/pull/7939) [`8f397ed`](https://github.com/Effect-TS/effect/commit/8f397ed4e7852026591819cc4c8c1995bdd2fb8c) Thanks @kitlangton! - Fix `Reply.Reply` codecs to require client services when decoding and server services when encoding. + +- [#7485](https://github.com/Effect-TS/effect/pull/7485) [`d7ae6b6`](https://github.com/Effect-TS/effect/commit/d7ae6b6491a88f2710612bfcddaf608ebe925f7c) Thanks @tim-smart! - Transient routing states for persisted cluster messages no longer surface as errors. + + If an entity moves runners or is shut down before replying, the caller keeps + waiting for the reply via message storage while the entity moves. If the local + runner is shutting down while a caller is waiting, the call is interrupted + instead of failing with `EntityNotAssignedToRunner`: the request is already + durable and will be served under the next owner. + + Durable workflows treat such an interrupt as an abandoned run attempt: the run + stops with nothing persisted, without running compensations or resuming the + parent, ready to replay on the replacement runner. + +- [#7933](https://github.com/Effect-TS/effect/pull/7933) [`8cf1203`](https://github.com/Effect-TS/effect/commit/8cf12030e8a3e1e76384884d41afa96e2bd72b13) Thanks @kitlangton! - Fix saved curried `Context.get` calls incorrectly inferring their required service as `unknown`. + +- [#8064](https://github.com/Effect-TS/effect/pull/8064) [`87654c5`](https://github.com/Effect-TS/effect/commit/87654c5eed150387f4eac7153751b6085616ba52) Thanks @Avaq! - Fix the `CookiesError` tag in `effect/unstable/http` from `CookieError` to `CookiesError` to match the class name. + +- [#7621](https://github.com/Effect-TS/effect/pull/7621) [`f05ae0b`](https://github.com/Effect-TS/effect/commit/f05ae0b3fd2cfc7f12b668af244b3cf3fae59bc5) Thanks @kitlangton! - Apply DateTime calendar parts without intermediate overflow. + +- [#7884](https://github.com/Effect-TS/effect/pull/7884) [`436f5eb`](https://github.com/Effect-TS/effect/commit/436f5eb44aa234bdf1de37b07da2ce8dc31ad4ec) Thanks @kitlangton! - Fix `ConfigProvider.fromDotEnvContents` variable expansion to preserve replacement tokens such as `$&` in referenced values. + +- [#7840](https://github.com/Effect-TS/effect/pull/7840) [`d8ff960`](https://github.com/Effect-TS/effect/commit/d8ff9605bb5ef7b0cacd80e3b43ed1ce9e3d5e36) Thanks @kitlangton! - Fix `DurableClock.sleep` to preserve explicit `0` and `0n` in-memory thresholds. + +- [#7941](https://github.com/Effect-TS/effect/pull/7941) [`8766475`](https://github.com/Effect-TS/effect/commit/876647502300b9fb79d163de10fd497716d93be0) Thanks @kitlangton! - Require schema encoding services when `DurableDeferred.into` records an exit. + +- [#7750](https://github.com/Effect-TS/effect/pull/7750) [`b64f406`](https://github.com/Effect-TS/effect/commit/b64f406de13a1b9da4664dbde2dbddaa5f776a05) Thanks @kitlangton! - Update dynamic tools to advertise replacement parameter schemas after `setParameters`. + +- [#7572](https://github.com/Effect-TS/effect/pull/7572) [`4697aaa`](https://github.com/Effect-TS/effect/commit/4697aaa1d38f9da7fe0bcb62f708183f766fe496) Thanks @tim-smart! - Align CLI help tables by terminal display width for wide, emoji, combining, and zero-width graphemes. + +- [#7588](https://github.com/Effect-TS/effect/pull/7588) [`cec6c2d`](https://github.com/Effect-TS/effect/commit/cec6c2d7bec7bea0ea179123af581a23c2e24494) Thanks @tim-smart! - Route tool call parameter validation failures through the tool's `failureMode` and drop `ToolParameterValidationError.toolParams`. + +- [#7643](https://github.com/Effect-TS/effect/pull/7643) [`9956f0e`](https://github.com/Effect-TS/effect/commit/9956f0e6f47096a6e31305ec2c7320b8e1a140af) Thanks @tim-smart! - Reduce memory usage in Effect primitives and fibers. + + Breaking: context-derived `Fiber` fields now live under `fiber.cache`. The + `currentScheduler`, `currentSpan`, `currentLogLevel`, `currentStackFrame`, and + `currentPreventYield` fields are now `scheduler`, `span`, `logLevel`, + `stackFrame`, and `preventYield`. Access `minimumLogLevel` and + `maxOpsBeforeYield` through `cache` as well. + +- [#7650](https://github.com/Effect-TS/effect/pull/7650) [`5c7eed0`](https://github.com/Effect-TS/effect/commit/5c7eed0c5aa0988716206dd374359223edf828ee) Thanks @tim-smart! - Reduce HTTP server allocation churn when tracing is not configured and for requests that complete synchronously. + +- [#7649](https://github.com/Effect-TS/effect/pull/7649) [`183c2ea`](https://github.com/Effect-TS/effect/commit/183c2ea65ee1e9b967c746aa1b0bf2b8d3422de2) Thanks @tim-smart! - Reduce per-request RPC server allocations. + +- [#7772](https://github.com/Effect-TS/effect/pull/7772) [`1e92dbd`](https://github.com/Effect-TS/effect/commit/1e92dbd26f5a74bb32103cbaac0f09460b7954de) Thanks @tim-smart! - Improve HTTP server throughput by reducing routing, request handling, response + construction, and body encoding overhead. Add `Effect.withFiberSucceed` for + synchronously computing successful values from the current fiber. Copy pooled + byte views by their exact range when exposing `ArrayBuffer` values. + +- [#7956](https://github.com/Effect-TS/effect/pull/7956) [`4eb0fa7`](https://github.com/Effect-TS/effect/commit/4eb0fa72302b0a63ac34c6515a9cdf96338e586a) Thanks @tim-smart! - Reduce HTTP server overhead: complete freshly created header maps in place in + `HttpServerResponse.setHeader` and `setHeaders`, compare static route prefixes + with a prepared `startsWith`, map `HttpApi` schema errors eagerly for completed + decoder results, and implement `Effect.cached` as a dedicated one-time memo + without time-to-live machinery. + +- [#7426](https://github.com/Effect-TS/effect/pull/7426) [`534b8b9`](https://github.com/Effect-TS/effect/commit/534b8b9dba195ec38a4795fe564d5e0876cb6468) Thanks @tim-smart! - Replace `@effect/sql-pg`'s `pg` runtime with a native PostgreSQL client. `PgConnection` and `PgPool` now handle connection setup, binary queries, prepared statements, pipelining, streaming, notifications, cancellation, and custom codecs. `PgConnection.listen` and `PgClient.listen` return scoped notification dequeues after PostgreSQL confirms the subscription. `PgClient` uses the native stack, and the legacy `fromPool`, `fromClient`, and `makeWith` constructors are removed. + + ### Breaking changes + + - `fromPool`, `fromClient`, and `makeWith` are removed. Use `make` for a pool or `makeClient` for one connection. + - `PgClient.listen` returns a scoped `Effect, SqlError, Scope>` instead of a `Stream`. Acquisition completes after PostgreSQL confirms `LISTEN`, so notifications sent after it returns cannot be missed. + - `PgClientConfig.types` now accepts a `PgTypes.Registry` instead of `pg.CustomTypesConfig`. Plain object parameters are no longer inferred as JSON; wrap them with `sql.json`. + - Query strings must contain one statement. PostgreSQL's extended protocol rejects multi-statement strings. + - Results use the native binary codecs. In particular, `int8` decodes to `bigint`, `date` to a string, timestamps to Unix epoch milliseconds, and `bytea` or unknown OIDs to `Uint8Array`. `executeRaw` returns the native `PgConnection.Result` shape rather than `pg.Result`. + - Named prepared statements are enabled by default. Set `prepare: false` when using a pooler that cannot preserve prepared statements between queries. `Statement.unprepared` and `Statement.valuesUnprepared` use unnamed extended queries without adding entries to the prepared-statement cache. + + Inferred parameters stay permissive: strings bind untyped so the backend derives the type from the statement, and safe integers beyond the `int4` range bind as `int8`. + + Add `Pool.reserve` for exclusive access to a concurrent pool item, and fix waiter wakeups and capacity replacement after invalidation. + +- [#7514](https://github.com/Effect-TS/effect/pull/7514) [`b76a1cf`](https://github.com/Effect-TS/effect/commit/b76a1cf32b0a742c24c1874137e2da8fed5c48eb) Thanks @tim-smart! - Add Socket.upgrade, for upgrading tcp sockets using STARTTLS + +- [#7568](https://github.com/Effect-TS/effect/pull/7568) [`acc1e53`](https://github.com/Effect-TS/effect/commit/acc1e5384aa5cb7b072f60308013f078b0099bdf) Thanks @tim-smart! - Normalize core service and runtime identities under their owning module namespaces. + +- [#8001](https://github.com/Effect-TS/effect/pull/8001) [`4950a91`](https://github.com/Effect-TS/effect/commit/4950a914a68306531a1111d4cdc4504aa8d3854e) Thanks @kitlangton! - Fix `Effect.fnUntracedEager` to pass the original function arguments to each transform after the current effect, matching `Effect.fn` and `Effect.fnUntraced`. + +- [#8005](https://github.com/Effect-TS/effect/pull/8005) [`c020987`](https://github.com/Effect-TS/effect/commit/c020987a0611c9e610a7eef347648b60026745b8) Thanks @kitlangton! - Fix `Effect.updateServiceScoped` cleanup when an inner service provider has already completed. Closing the scope now preserves the service's absence instead of failing with a missing-service defect. + +- [#8007](https://github.com/Effect-TS/effect/pull/8007) [`abe95d1`](https://github.com/Effect-TS/effect/commit/abe95d12340e1dd435e5df623b1967bf01938c94) Thanks @kitlangton! - Preserve the original cause in `Effect.catchReason` and `Effect.catchReasons` when no nested reason matches and no fallback is provided. + +- [#7915](https://github.com/Effect-TS/effect/pull/7915) [`027ceb9`](https://github.com/Effect-TS/effect/commit/027ceb91e003fbc36f0559f72e1c614cd9198c1a) Thanks @kitlangton! - Fix `Effectable.Class` evaluation by delegating to its abstract `asEffect()` method. The method is called on the instance for each execution, preserving current receiver state and provided services. + +- [#7907](https://github.com/Effect-TS/effect/pull/7907) [`3d203b7`](https://github.com/Effect-TS/effect/commit/3d203b702e26687d2fea4aaae7cc01dcfb82c05e) Thanks @KhraksMamtsov! - Add `Effectable.Mixin` to insert the Effect prototype into an existing class inheritance chain. The returned abstract class requires an `asEffect` method and derives its Effect type from that method through polymorphic `this`. + +- [#8009](https://github.com/Effect-TS/effect/pull/8009) [`a29b8f4`](https://github.com/Effect-TS/effect/commit/a29b8f4de5aee58dd9ee645365c04cfe1ea6fa9f) Thanks @kitlangton! - Fix the `onError` and `onSyncError` argument tuple types in `Effect.effectify` to include only caller inputs, excluding the synthesized callback. Mapper annotations that expected a callback slot must use the caller-input tuple instead. Runtime behavior is unchanged. + +- [#7902](https://github.com/Effect-TS/effect/pull/7902) [`ce4aa65`](https://github.com/Effect-TS/effect/commit/ce4aa65409e8500908fb6496eca06270ef5b636f) Thanks @kitlangton! - Fix `EntityProxyServer` handler layers to include client-side codec service requirements. + +- [#7889](https://github.com/Effect-TS/effect/pull/7889) [`a8ea807`](https://github.com/Effect-TS/effect/commit/a8ea80799a0bf187c577e052fef0b7245494c7bf) Thanks @kitlangton! - Honor the entity layer's `disableFatalDefects` option in `Entity.makeTestClient`. When enabled, a handler defect no longer fails other pending calls to the same entity ID. The failing call still reports its defect; omitted or false options retain fatal-defect behavior. + +- [#7862](https://github.com/Effect-TS/effect/pull/7862) [`a3ebb7f`](https://github.com/Effect-TS/effect/commit/a3ebb7fcb208f3933a40a0fd949dbcfdf1ca2921) Thanks @kitlangton! - Retry `EventLogRemote` writes and change streams when authentication returns `Forbidden`. + +- [#7842](https://github.com/Effect-TS/effect/pull/7842) [`473bd81`](https://github.com/Effect-TS/effect/commit/473bd814a97dba66070435cfe84933bcc143effe) Thanks @kitlangton! - Return one empty chunk when `ChunkedMessage.split` receives an empty `Uint8Array`. + +- [#7525](https://github.com/Effect-TS/effect/pull/7525) [`53843f6`](https://github.com/Effect-TS/effect/commit/53843f6490f4eebaf1eeb91fdcc5f1c542b0e132) Thanks @fubhy! - Add `ByteSize` module and use it across the ecosystem + +- [#8115](https://github.com/Effect-TS/effect/pull/8115) [`c5eca65`](https://github.com/Effect-TS/effect/commit/c5eca6539f0d605acd5ce19437fa88f167d69bb7) Thanks @tim-smart! - Calculate `HttpBody.file`, `HttpBody.fileFromInfo`, and `HttpClientRequest.bodyFile` content lengths with exact bigint arithmetic and EOF clamping. + +- [#7784](https://github.com/Effect-TS/effect/pull/7784) [`d6f9eba`](https://github.com/Effect-TS/effect/commit/d6f9eba7e8df0c8bd74762c313402884d94ba146) Thanks @kitlangton! - Ensure `ExecutionPlan.captureRequirements` provides captured services to effectful `while` predicates. + +- [#7460](https://github.com/Effect-TS/effect/pull/7460) [`8d1e97a`](https://github.com/Effect-TS/effect/commit/8d1e97adbf5a36b4b53ab56f797c2ec0267a8821) Thanks @tim-smart! - Fix contextual typing for `Match` tag and discriminator handler maps when handlers use `Effect.fn` or `Effect.fnUntraced`. + +- [#8070](https://github.com/Effect-TS/effect/pull/8070) [`b28ab48`](https://github.com/Effect-TS/effect/commit/b28ab48be7cf7f2bd30f9467ccac0ab41260f7ad) Thanks @tim-smart! - Fix parallel child workflows inside activities to dispatch before suspending, release activity resources during durable waits, and resume reliably when children complete during cleanup. + +- [#7677](https://github.com/Effect-TS/effect/pull/7677) [`9960708`](https://github.com/Effect-TS/effect/commit/9960708052fcaaa873c471aea144ca46955e6023) Thanks @kitlangton! - Set `duplex` for raw Web stream request bodies in `FetchHttpClient`. + +- [#7615](https://github.com/Effect-TS/effect/pull/7615) [`8ac53b6`](https://github.com/Effect-TS/effect/commit/8ac53b6ae677f06d88d7d81f932ab5ff9b59a10e) Thanks @kitlangton! - Fix `FiberMap` losing track of fibers started under the same key by a replaced fiber's synchronous finalizer, ensuring they are interrupted when the map's scope closes. + +- [#7967](https://github.com/Effect-TS/effect/pull/7967) [`84ad49a`](https://github.com/Effect-TS/effect/commit/84ad49a357ef156b4ff46cefc945efbe3e292eff) Thanks @kitlangton! - Preserve an already registered fiber when `FiberHandle` or `FiberMap` registers it again with `onlyIfMissing: true`, instead of interrupting it and clearing the entry. + +- [#8083](https://github.com/Effect-TS/effect/pull/8083) [`72cfa24`](https://github.com/Effect-TS/effect/commit/72cfa2420977919ba57a8e6de538e01e06d5f380) Thanks @nikelborm! - Exposed the platform specific pretty loggers separately. + +- [#7788](https://github.com/Effect-TS/effect/pull/7788) [`95c2581`](https://github.com/Effect-TS/effect/commit/95c2581e0f78e1537aad21764fe562206ca50adb) Thanks @kitlangton! - Fix `FileSystem.sink` to retain its default write flag when `flag` is undefined. + +- [#8074](https://github.com/Effect-TS/effect/pull/8074) [`d5c7cd2`](https://github.com/Effect-TS/effect/commit/d5c7cd2c47b2b01523aaea48d2dd039b89b36114) Thanks @nikelborm! - Removed unused stderr option from Logger.consolePretty signature + +- [#7965](https://github.com/Effect-TS/effect/pull/7965) [`fe4fed1`](https://github.com/Effect-TS/effect/commit/fe4fed1b014c78043a8efe37520c527100910010) Thanks @kitlangton! - Match `AtomHttpApi` query and mutation success types to the generated HTTP client, + including SSE, binary streams, and header-wrapped responses. Stream transport, + decoding, and SSE errors now appear in the stream's error channel instead of + `never`, so code that assumed a failure-free stream may need to handle them. + Runtime and serialization behavior are unchanged. + +- [#7959](https://github.com/Effect-TS/effect/pull/7959) [`d150a64`](https://github.com/Effect-TS/effect/commit/d150a643c4f60da2dc19ae813b807d48b580087e) Thanks @kitlangton! - Fix `AtomHttpApi` query and mutation dispatch for top-level API groups. + +- [#7961](https://github.com/Effect-TS/effect/pull/7961) [`05b1e80`](https://github.com/Effect-TS/effect/commit/05b1e80cde3a8da1ff1afe288f8539b72c05bb87) Thanks @kitlangton! - Honor explicit `timeToLive: 0` and `timeToLive: 0n` in `AtomRpc` and `AtomHttpApi` queries. Zero now opts out of the registry's default idle retention, matching other zero-duration inputs, so an unmounted query can be disposed and fetched again on remount. Omitting `timeToLive` still uses the registry default. + +- [#7963](https://github.com/Effect-TS/effect/pull/7963) [`414dc90`](https://github.com/Effect-TS/effect/commit/414dc9056a39aa22f7cc27aeb5e631231ce50982) Thanks @kitlangton! - Fix `AtomRpc` mutation and query atoms to include client middleware errors in their result error types. + +- [#7740](https://github.com/Effect-TS/effect/pull/7740) [`3f51acd`](https://github.com/Effect-TS/effect/commit/3f51acdf5e2490d7d0a833cd67ce06d1d867facd) Thanks @kitlangton! - Forward `Atom.withFallback` writes to the primary atom. + +- [#7693](https://github.com/Effect-TS/effect/pull/7693) [`d68ff05`](https://github.com/Effect-TS/effect/commit/d68ff05506ab5bb3ec07e1b759ce296423b0dbbe) Thanks @kitlangton! - Fix `HttpMiddleware.cors` to preserve `Origin` and other required `Vary` dimensions. + +- [#8140](https://github.com/Effect-TS/effect/pull/8140) [`f3cf1e6`](https://github.com/Effect-TS/effect/commit/f3cf1e6c36f1aac8399d9b540aecb6410be3096f) Thanks @gcanti! - Fix `Types.DeepMutable` to preserve built-in objects, Effect data types, and other objects with methods or symbol-keyed properties while recursively making arrays, tuples, maps, sets, and plain records mutable. + +- [#7695](https://github.com/Effect-TS/effect/pull/7695) [`6525771`](https://github.com/Effect-TS/effect/commit/6525771eef7df6b71a3ef6e299ee93d798ed3dda) Thanks @kitlangton! - Use body status and encoding defaults in `HttpApiSchema.encodeToWithHeaders`. + +- [#8110](https://github.com/Effect-TS/effect/pull/8110) [`4ab4e83`](https://github.com/Effect-TS/effect/commit/4ab4e838a4c9ab3c9cc0d52d3e83de88686601b0) Thanks @tim-smart! - Prevent precision loss in Node/Bun filesystem operations and `HttpPlatform` file responses. + +- [#8111](https://github.com/Effect-TS/effect/pull/8111) [`f7f1d78`](https://github.com/Effect-TS/effect/commit/f7f1d78e52023bdfbebf76949c550db8bb9411dd) Thanks @tim-smart! - Clamp HttpPlatform file response ranges to the file size so Content-Length matches the bytes available. Oversized reads stop at EOF, and offsets at or past EOF return an empty body with Content-Length 0. Apply the same clamping to the default Web file response implementation. + +- [#7699](https://github.com/Effect-TS/effect/pull/7699) [`47b358a`](https://github.com/Effect-TS/effect/commit/47b358a4b85d22973a7957abd1a859f65b75b7da) Thanks @kitlangton! - Fix `HttpApiClient` decoding form-urlencoded responses. + +- [#7705](https://github.com/Effect-TS/effect/pull/7705) [`45ffa72`](https://github.com/Effect-TS/effect/commit/45ffa7221259c7e4be9797b90f6ca569fe7ec8a3) Thanks @kitlangton! - Run registered pre-response handlers before `HttpApiTest` returns responses. + +- [#7701](https://github.com/Effect-TS/effect/pull/7701) [`6232650`](https://github.com/Effect-TS/effect/commit/62326505a74fcd8fe27c8e2064128d2dea484ecc) Thanks @kitlangton! - Fix `HttpApiClient.urlBuilder` dropping base URL pathnames. + +- [#7661](https://github.com/Effect-TS/effect/pull/7661) [`4b73e1b`](https://github.com/Effect-TS/effect/commit/4b73e1bd5576346d7939be8915f3053e428d1c0e) Thanks @kitlangton! - Add `JsonPointer.parseUriFragment` and `JsonPointer.formatUriFragment` for converting RFC 6901 URI fragments, and use them to preserve percent-encoded definition names in exported JSON Schema references. JSON Schema compilation now rejects malformed local definition references returned by `toJsonSchema` hooks. Such hooks must percent-encode characters that URI fragments do not permit, for example `%` as `%25` and `#` as `%23`. + +- [#7675](https://github.com/Effect-TS/effect/pull/7675) [`284050c`](https://github.com/Effect-TS/effect/commit/284050c2aa6e78eceb08e5eff1803501cc80a658) Thanks @kitlangton! - Normalize MIME type parameters and whitespace in `Mime.getAllExtensions`. + +- [#8108](https://github.com/Effect-TS/effect/pull/8108) [`c85fc0b`](https://github.com/Effect-TS/effect/commit/c85fc0b8f2195780929d6fc8ecdc084faf18736c) Thanks @tim-smart! - On Node and Deno, `FileSystem.File.seek` now rejects negative resulting positions with a `BadArgument` platform error, leaving the cursor unchanged. Its return type is now `Effect`. + +- [#8148](https://github.com/Effect-TS/effect/pull/8148) [`7999b07`](https://github.com/Effect-TS/effect/commit/7999b078950fda15e235db1e61e8db5eb0632528) Thanks @tim-smart! - Preserve the response Content-Type when compressing file, raw, stream, and byte-array responses on Node, Bun, Deno, and the web platform, including headers overridden after body construction. + +- [#7703](https://github.com/Effect-TS/effect/pull/7703) [`7d455f5`](https://github.com/Effect-TS/effect/commit/7d455f51acf2f0e9f710f298e14e4d81179a9259) Thanks @kitlangton! - Apply endpoint OpenAPI overrides and transforms after schema generation. + +- [#8057](https://github.com/Effect-TS/effect/pull/8057) [`d681c2e`](https://github.com/Effect-TS/effect/commit/d681c2eb1bb07d782540286368e8b42533df3063) Thanks @kitlangton! - Fix `Prompt.date` carrying typed digits into the next field when pressing Tab, including when navigation wraps. + +- [#7742](https://github.com/Effect-TS/effect/pull/7742) [`84d2a47`](https://github.com/Effect-TS/effect/commit/84d2a470ecc0fcad7f1d1e7a50464e02af0e6972) Thanks @kitlangton! - Prevent `Reactivity.query` cleanup from failing when keys are repeated. + +- [#7659](https://github.com/Effect-TS/effect/pull/7659) [`ed74b18`](https://github.com/Effect-TS/effect/commit/ed74b18b95b0797808f2f7928f413f1cf2279d77) Thanks @kitlangton! - Preserve pending leftovers when a `Sink.flatMap` continuation completes without consuming input. + +- [#7671](https://github.com/Effect-TS/effect/pull/7671) [`2245997`](https://github.com/Effect-TS/effect/commit/2245997ad2214591b7d9674777595092926743cf) Thanks @kitlangton! - Preserve SSE events with mixed line endings. + +- [#7691](https://github.com/Effect-TS/effect/pull/7691) [`d386979`](https://github.com/Effect-TS/effect/commit/d386979fedbdd433d28816f9cc74675a9af45189) Thanks @kitlangton! - Ignore Range headers on non-GET requests in HttpStaticServer. + +- [#8109](https://github.com/Effect-TS/effect/pull/8109) [`fc9fedf`](https://github.com/Effect-TS/effect/commit/fc9fedf59fc26a8bc6e45c3860a921f3b7e0f971) Thanks @tim-smart! - Parse HttpStaticServer byte range integers exactly, including values above Number.MAX_SAFE_INTEGER. Oversized starts now return 416 with Content-Range instead of falling back to 200. Oversized ends clamp to the last byte, and oversized suffixes return the whole file as 206. + +- [#7697](https://github.com/Effect-TS/effect/pull/7697) [`7750dbe`](https://github.com/Effect-TS/effect/commit/7750dbe0d5b9929c6d92b0365b42784b993ea2a6) Thanks @kitlangton! - Fix `HttpApiBuilder` ignoring the status annotation on a `HttpApiSchema.WithHeaders` wrapper around a streaming success, which defected when the wrapper and inner statuses differed. + +- [#7667](https://github.com/Effect-TS/effect/pull/7667) [`fc91af6`](https://github.com/Effect-TS/effect/commit/fc91af659d0682659e6347eede288f90f618acc0) Thanks @kitlangton! - Fix `Toml.parse` rejecting child tables in separate array-of-tables entries. + +- [#8106](https://github.com/Effect-TS/effect/pull/8106) [`39b9738`](https://github.com/Effect-TS/effect/commit/39b9738727ab4d87298fd640edd138f352c70ee3) Thanks @tim-smart! - Fix tool result serialization to select the codec using `isFailure` and preserve `encodedResult` through `Response.AllParts` round trips. + + Add `Tool.failureResultSchema(tool)` and `Tool.ExecutionFailure` to handle user failures, `AiError`, and denied or interrupted calls consistently. Also export `HttpRequestDetails` and `HttpResponseDetails` from `AiError`; the `Response` exports remain available. + + ### Breaking changes + + - Stored results must match the selected schema. With success `Schema.Number` and failure `Schema.NumberFromString`, migrate failed results from `404` to `"404"`. + - `Response.ToolResultPart` returns `Schema.Codec` instead of `Schema.decodeTo`. Update annotations that depend on the old type. + - `Tool.FailureResult` and `Tool.Result`, including their encoded variants, now include `Tool.ExecutionFailure` in both failure modes. Handle it when narrowing failed results. + +- [#8132](https://github.com/Effect-TS/effect/pull/8132) [`88093b5`](https://github.com/Effect-TS/effect/commit/88093b57951a1b597599fe4772ab690b36a95818) Thanks @LeonardoTrapani! - Fix activity count leaks when workflow activity acquisition is interrupted, which could block later workflow suspension. + +- [#7669](https://github.com/Effect-TS/effect/pull/7669) [`d14c463`](https://github.com/Effect-TS/effect/commit/d14c4631ac82911a8ca0b6c24c14ecb95863ee6b) Thanks @kitlangton! - Fix folded YAML scalars to preserve paragraph and indentation breaks. + +- [#7872](https://github.com/Effect-TS/effect/pull/7872) [`d473bd3`](https://github.com/Effect-TS/effect/commit/d473bd3ee966a3a0c245d0e3c752fd91368b8ff6) Thanks @kitlangton! - Preserve defined falsy Error causes (`0`, `false`, `""`, `null`, `0n`, and `NaN`) in `Formatter.format` output. Missing and explicitly `undefined` causes remain omitted. + +- [#7451](https://github.com/Effect-TS/effect/pull/7451) [`84864bc`](https://github.com/Effect-TS/effect/commit/84864bc30c9e92a1226f65bb78b0641a7e0acea2) Thanks @gcanti! - Fix equivalence derivation for schema class APIs by adopting the equivalence of + their declared fields. Class declarations previously fell back to + `Equal.equals`, which also compared runtime properties outside the schema and + could make field-equivalent class instances compare as unequal. + +- [#7920](https://github.com/Effect-TS/effect/pull/7920) [`e2ae724`](https://github.com/Effect-TS/effect/commit/e2ae72481e755a454f28ce45e90f43b857b7c6e5) Thanks @kitlangton! - Fix `Graph.bellmanFord` reporting a negative cycle as affecting a target across an impassable, positive-infinite-weight edge. Targets separated from the cycle by such edges now retain their finite shortest path or remain unreachable, while targets reachable from the cycle through finite-weight edges still report an error. + +- [#7625](https://github.com/Effect-TS/effect/pull/7625) [`f921ed3`](https://github.com/Effect-TS/effect/commit/f921ed305c63415dc468e94d9319cc3237119138) Thanks @kitlangton! - Prevent `HashMap` iterators from exposing mutable internal collision entries. + +- [#7886](https://github.com/Effect-TS/effect/pull/7886) [`1df933d`](https://github.com/Effect-TS/effect/commit/1df933d1dee2e09a25f9c27214b31aa94fa9d020) Thanks @kitlangton! - Fix `HashRing.getShards` skipping an eligible node at the first ring position when other nodes have reached their allocation quota. + +- [#7629](https://github.com/Effect-TS/effect/pull/7629) [`829aff9`](https://github.com/Effect-TS/effect/commit/829aff97d71d7900b7765ba2f63e33874b898cdc) Thanks @kitlangton! - Compare header names case-insensitively in `Headers.isRedactedName`. + +- [#7627](https://github.com/Effect-TS/effect/pull/7627) [`aa0aba3`](https://github.com/Effect-TS/effect/commit/aa0aba383382ca1b99fbe618dceba63c3a5a6a53) Thanks @kitlangton! - Fix `Headers.redact` and `Headers.isRedactedName` skipping matches when a redaction pattern is a global or sticky regular expression. + +- [#7945](https://github.com/Effect-TS/effect/pull/7945) [`a71140f`](https://github.com/Effect-TS/effect/commit/a71140fbe79809aa5311a55b1c6908905dc2df26) Thanks @kitlangton! - Constrain the data-first `HttpClient.catch(client, recover)` overload to recover with + `HttpClientResponse` values, matching the data-last overload. Callbacks returning other + success types are now rejected; use `Effect.catch` on the result of `client.execute(request)` + to recover to arbitrary values. + +- [#7922](https://github.com/Effect-TS/effect/pull/7922) [`0276a27`](https://github.com/Effect-TS/effect/commit/0276a27ddbb15d163daf56f059ca9f62c4fd9939) Thanks @kitlangton! - Fix `HttpClient.followRedirects` bypassing response-level recovery when request preprocessing fails. + +- [#8131](https://github.com/Effect-TS/effect/pull/8131) [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb) Thanks @gcanti! - Fix `HttpClientResponse.schemaJson` and `HttpClientResponse.schemaNoBody` to apply parse options when decoding response + schemas. + +- [#7679](https://github.com/Effect-TS/effect/pull/7679) [`c86c999`](https://github.com/Effect-TS/effect/commit/c86c99939e1628813a2f92f0906cfbbe924504e8) Thanks @kitlangton! - Close request scopes for streaming HEAD responses. + +- [#7681](https://github.com/Effect-TS/effect/pull/7681) [`975f758`](https://github.com/Effect-TS/effect/commit/975f7586f3dcda5743d6570a740013a9fbdce9a7) Thanks @kitlangton! - Preserve `Content-Length` headers in `HttpServerResponse.fromWeb`. + +- [#7689](https://github.com/Effect-TS/effect/pull/7689) [`2a3a478`](https://github.com/Effect-TS/effect/commit/2a3a4783bcd2d4e91e3505c7a6c1e398fea89595) Thanks @kitlangton! - Normalize router prefixes before removing them from handler request URLs. + +- [#7898](https://github.com/Effect-TS/effect/pull/7898) [`1e6e206`](https://github.com/Effect-TS/effect/commit/1e6e206f6327a114f55a541775f465c0bc9fbf3e) Thanks @kitlangton! - Fix `HttpRunner` HTTP and WebSocket client URLs adding an extra leading slash to slash-prefixed paths. Insert the address/path separator only when it is missing, preserving intentional leading and interior slashes. + + This path correction is normally masked by router normalization, but prevents route misses for non-root paths when duplicate-slash normalization is disabled. Applications that compensate for the extra slash may need to remove that compensation. Router defaults and shared trailing-slash handling are unchanged. + +- [#7927](https://github.com/Effect-TS/effect/pull/7927) [`fa6027b`](https://github.com/Effect-TS/effect/commit/fa6027b16e65376078e526fa33116ac0cc82485b) Thanks @tim-smart! - Reduce cold start cost of `HttpRouter` and `HttpEffect` web handlers. + + - `HttpServerRespondable` no longer imports `Schema` to detect schema errors, which removes the Schema modules from bundles that do not otherwise use them (about 23% of a minimal `HttpRouter` bundle). + - `HttpRouter.toWebHandler`, `HttpEffect.toWebHandlerLayer` and `HttpEffect.toWebHandlerLayerWith` now build the layer immediately instead of on the first request. A failed build never surfaces as an unhandled rejection; every request rejects with the build error instead. + +- [#7561](https://github.com/Effect-TS/effect/pull/7561) [`7616f73`](https://github.com/Effect-TS/effect/commit/7616f73c16bd45a3a3e2202d3f81c6d1cc63f4e0) Thanks @jensdev! - Fix `HttpApiMiddleware`-declared errors being duplicated and mis-encoded. + +- [#7954](https://github.com/Effect-TS/effect/pull/7954) [`fc668b6`](https://github.com/Effect-TS/effect/commit/fc668b61e9755545a47582dee6c4425d5652cbb6) Thanks @fitchmultz! - Allow generated `HttpApiClient` methods and `AtomHttpApi` queries and mutations to accept native SSE decode options per call through the request's `sseOptions` field. + +- [#7994](https://github.com/Effect-TS/effect/pull/7994) [`d425c8c`](https://github.com/Effect-TS/effect/commit/d425c8c1319519e861714bcb9d055b63a9434fad) Thanks @tim-smart! - Prevent unencodable atom values from aborting dehydration of the rest of an atom registry. + +- [#7935](https://github.com/Effect-TS/effect/pull/7935) [`ce120f4`](https://github.com/Effect-TS/effect/commit/ce120f474b87d8a5a787dd7f848177ecd3b960fc) Thanks @kitlangton! - Fix `Layer.tapError` and `Layer.tapCause` to require observers that accept the source layer's complete error type. + +- [#7983](https://github.com/Effect-TS/effect/pull/7983) [`248201f`](https://github.com/Effect-TS/effect/commit/248201f7846d855685a4dc1b2f01968981706d69) Thanks @kitlangton! - Honor `captureStackTrace` in both forms of `Layer.withSpan`. Layer construction diagnostics previously reported a location inside `Layer.ts` instead of the `withSpan` call site, and ignored `captureStackTrace: false` or a supplied lazy stack. + +- [#7937](https://github.com/Effect-TS/effect/pull/7937) [`e80d397`](https://github.com/Effect-TS/effect/commit/e80d3972fcca4e986b6497d67406f45ee1be540d) Thanks @kitlangton! - Preserve resource acquisition errors on `LayerMap.Service` when `preload: true` is set. The yielded service instance and its `get`, `contextEffect`, and `contextEffectOption` accessors now retain the resource error type because a resource can fail when reacquired, even if preloading succeeded. + + Consumers that assumed these accessors had a `never` error must handle the resource error. Runtime behavior is unchanged. + +- [#7874](https://github.com/Effect-TS/effect/pull/7874) [`f1a941d`](https://github.com/Effect-TS/effect/commit/f1a941d52c2f71973e998883869e4e9ef510abf2) Thanks @kitlangton! - Fix `Logger.toFile` dropping the remainder of a log batch when a successful file write writes only part of the buffer. File logging now uses the complete-write contract; write errors continue to be ignored. + +- [#7814](https://github.com/Effect-TS/effect/pull/7814) [`73bc3a1`](https://github.com/Effect-TS/effect/commit/73bc3a13b6e1b7a060e6bcd19ae7796443211a80) Thanks @kitlangton! - Fix `McpServer` HTTP resource templates failing to resolve. + +- [#7816](https://github.com/Effect-TS/effect/pull/7816) [`b628bb1`](https://github.com/Effect-TS/effect/commit/b628bb1240a09df2244c3ff6c587f978feb25225) Thanks @kitlangton! - Fix `McpServer.registerPrompt` callback types to use decoded prompt parameters. + +- [#7495](https://github.com/Effect-TS/effect/pull/7495) [`6e3ae7b`](https://github.com/Effect-TS/effect/commit/6e3ae7b6359bda08cea74b761fd54224d7af45e2) Thanks @IMax153! - McpServer no longer sends `null` or array tool results as `structuredContent`, which MCP requires to be a JSON object. + +- [#7835](https://github.com/Effect-TS/effect/pull/7835) [`e891247`](https://github.com/Effect-TS/effect/commit/e8912473bd4128714a0f8cb04621f3e12f62a910) Thanks @kitlangton! - Remove queued control envelopes when clearing an address from in-memory message storage. + +- [#7993](https://github.com/Effect-TS/effect/pull/7993) [`53e6c73`](https://github.com/Effect-TS/effect/commit/53e6c7311b948a62e93e57f6daee66a8ec24b030) Thanks @kitlangton! - Ensure metrics with equal attributes share a series regardless of attribute insertion order. + +- [#7991](https://github.com/Effect-TS/effect/pull/7991) [`1579d6f`](https://github.com/Effect-TS/effect/commit/1579d6fd063e444e4fcdfd08ff194c7485203f9c) Thanks @kitlangton! - Fix metrics reused across different `MetricRegistry` services to read and update the active registry while preserving each registry's values when revisited. + +- [#7665](https://github.com/Effect-TS/effect/pull/7665) [`d3c6b73`](https://github.com/Effect-TS/effect/commit/d3c6b73cc4b78f22fdb2e25c8bebda027551540b) Thanks @kitlangton! - Fix `Model.FieldOption` to preserve omitted variants. + +- [#7987](https://github.com/Effect-TS/effect/pull/7987) [`4a59c6a`](https://github.com/Effect-TS/effect/commit/4a59c6a559e69d399b00c6acd48c8e1977e34875) Thanks @kitlangton! - Add `Multipart.isStreamPart` to recognize only a text `Field` or streamed `File`, while preserving `Multipart.isPart` for all branded multipart parts, including `PersistedFile` values. + +- [#7519](https://github.com/Effect-TS/effect/pull/7519) [`145d8e1`](https://github.com/Effect-TS/effect/commit/145d8e1013220425b8edf34f7011c73f73e1cdcf) Thanks @gcanti! - Fix `Schema.mutable` to preserve array and tuple metadata and reject node-level encodings. + +- [#7776](https://github.com/Effect-TS/effect/pull/7776) [`f74282c`](https://github.com/Effect-TS/effect/commit/f74282cb08de808b678ce67687acbcd61939d2f6) Thanks @kitlangton! - Preserve values added to an empty `MutableList` by `prependAll` when appending more values. + +- [#7571](https://github.com/Effect-TS/effect/pull/7571) [`0a38623`](https://github.com/Effect-TS/effect/commit/0a3862349ebfbc41746aa0de5e682992719c1da6) Thanks @tim-smart! - Export the `Random.Random` service interface and `Metric.MetricRegistry` type so custom service implementations can be annotated without accessing `Context.Reference` phantom types. + +- [#7524](https://github.com/Effect-TS/effect/pull/7524) [`0a08ae0`](https://github.com/Effect-TS/effect/commit/0a08ae0626f8124779ec2e32ae2088d28db157c6) Thanks @fubhy! - Add `NetAddress` under `effect/unstable/net` for MAC, IP, internet socket, and Unix socket addresses, with checked parsing, schemas, equality, canonical string serialization, and URL formatting. Companion modules `IpInterface` and `IpNetwork` represent IP interfaces and CIDR networks. + + HTTP and socket servers now expose `NetAddress.SocketAddress`. Replace TCP `hostname` access with `NetAddress.formatIp(address.address)` and use `UnixPathAddress.path` for Unix sockets. URL helpers bracket IPv6 addresses and reject scoped IPv6. Bun and Deno HTTP server layers can now fail with `ServeError` when listener address conversion fails. + + PostgreSQL `inet` values now use `IpInterface`; `cidr` values use `IpNetwork` and reject addresses with host bits set. + +- [#7443](https://github.com/Effect-TS/effect/pull/7443) [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72) Thanks @youngspe! - Terminate the `Stream.fromEventListener` stream after one item if `once: true`. + +- [#7510](https://github.com/Effect-TS/effect/pull/7510) [`d60c5d4`](https://github.com/Effect-TS/effect/commit/d60c5d40b5954ea1557aef1502b4b87f98bbf134) Thanks @gcanti! - Normalize numeric collection and batch counts across `Stream`, `Channel`, `Sink`, `MutableList`, `RequestResolver`, `Queue`, `TxQueue`, `PubSub`, and `HashRing`, preventing fractional, `NaN`, and non-positive counts from producing incorrect output, exceptions, waits for the wrong batch size, or non-terminating pulls. + +- [#7910](https://github.com/Effect-TS/effect/pull/7910) [`07ffd25`](https://github.com/Effect-TS/effect/commit/07ffd25bd0e82c9e6b72b8e669833fb4531a9da3) Thanks @kitlangton! - Fix `Number.remainder` to preserve negative-zero dividends with ordinary finite divisors. + +- [#7547](https://github.com/Effect-TS/effect/pull/7547) [`9b517ad`](https://github.com/Effect-TS/effect/commit/9b517ad28a0c2f213693bcab408658c40e3b6d9b) Thanks @tim-smart! - Improve `PersistedQueue` reliability across SQL, Redis, and memory stores. Retry policy now lives on `make()`, attempts count on claim, retries follow a `Schedule`, and exhausted or undecodable elements are dead-lettered. Add retention cleanup, durable acknowledgement retries, storage schema fixes, local poll wakeups, and fixes for the memory take race and Redis dedup growth. + +- [#7778](https://github.com/Effect-TS/effect/pull/7778) [`604b1c1`](https://github.com/Effect-TS/effect/commit/604b1c109d6c9e654b9b7fcc98d6b80361400325) Thanks @kitlangton! - Ensure `Optic.pick` and `Optic.omit` delete focused optional fields omitted from a replacement. + +- [#7780](https://github.com/Effect-TS/effect/pull/7780) [`ccc2e02`](https://github.com/Effect-TS/effect/commit/ccc2e02def5965f5535ca97fac2051f4387c4e13) Thanks @kitlangton! - Fix `Optic.optionalKey` to splice tuple elements selected by string indices. + +- [#7782](https://github.com/Effect-TS/effect/pull/7782) [`14d810a`](https://github.com/Effect-TS/effect/commit/14d810a726caad8a7b0d42f04a6876f25fbcc005) Thanks @kitlangton! - Fix `Order.combineAll` consuming one-shot iterables after the first comparison. + +- [#7931](https://github.com/Effect-TS/effect/pull/7931) [`a9d1ee3`](https://github.com/Effect-TS/effect/commit/a9d1ee3d4d51e97ea33440fe6100935d5fd5aada) Thanks @tim-smart! - Fix disabled OTLP batching to skip empty exports and avoid resending buffered items. + +- [#7929](https://github.com/Effect-TS/effect/pull/7929) [`a31adbe`](https://github.com/Effect-TS/effect/commit/a31adbefd6c04c70536de11433edba9edd1f5013) Thanks @tim-smart! - Speed up `OtlpTracer` span creation and export. Spans now allocate identifiers, attributes, and events lazily, and `Encoding.randomHex` produces flat strings for 16 and 32 character identifiers so serialization no longer flattens ropes. + +- [#7584](https://github.com/Effect-TS/effect/pull/7584) [`7245f87`](https://github.com/Effect-TS/effect/commit/7245f87ace09806d15893df8f0bac8f0b9b817db) Thanks @kitlangton! - Fix `PartitionedSemaphore` leaving a new waiter suspended when a previously resumed waiter for the same partition is interrupted before its acquisition completes. + +- [#7766](https://github.com/Effect-TS/effect/pull/7766) [`3a0828b`](https://github.com/Effect-TS/effect/commit/3a0828b058abb4b4646bd880458b4e636d77d1c3) Thanks @kitlangton! - Persist synchronous defects thrown by `PersistedCache` lookups. + +- [#7604](https://github.com/Effect-TS/effect/pull/7604) [`cd83544`](https://github.com/Effect-TS/effect/commit/cd83544dd594b99e8ea0d7a7645c086f5861769b) Thanks @kitlangton! - Keep `Pool.reserve` items out of shared circulation when other borrowers return or overlapping reservations close. Restore available slots only after the last reservation closes. + +- [#8078](https://github.com/Effect-TS/effect/pull/8078) [`6550a07`](https://github.com/Effect-TS/effect/commit/6550a074c5c501d8a95dcd354f17e9785a1ae1eb) Thanks @tim-smart! - Port `HttpApiBuilder.handler` from v3 to define reusable endpoint callbacks with inferred request, response, error, and service types. + +- [#7663](https://github.com/Effect-TS/effect/pull/7663) [`6f090d4`](https://github.com/Effect-TS/effect/commit/6f090d4c2032cc35f7f9a9eac900d0329b8ac07e) Thanks @kitlangton! - Preserve schema classes when extracting their default `VariantSchema` variant. + +- [#7760](https://github.com/Effect-TS/effect/pull/7760) [`b505c0d`](https://github.com/Effect-TS/effect/commit/b505c0db03e86eb126237690a5867f457eb4237f) Thanks @kitlangton! - Preserve negative counter deltas in OTLP and OpenTelemetry metric exports. + +- [#7478](https://github.com/Effect-TS/effect/pull/7478) [`186dd49`](https://github.com/Effect-TS/effect/commit/186dd4914084fa346a870ce9c637b9c2a6cc8100) Thanks @gcanti! - Normalize numeric collection counts consistently across `Array`, `Chunk`, `Iterable`, and `String`, and make `TupleOf` fall back to `Array` for positive fractional lengths. + +- [#8097](https://github.com/Effect-TS/effect/pull/8097) [`9f37e58`](https://github.com/Effect-TS/effect/commit/9f37e58d00e06150d96af48fffe780947ca0d519) Thanks @tim-smart! - Keep the previous prompt frame visible until the next frame or submission is ready to display. + +- [#7637](https://github.com/Effect-TS/effect/pull/7637) [`4446451`](https://github.com/Effect-TS/effect/commit/44464510114773e1ebd1abb6efffa382cf69998e) Thanks @kitlangton! - Preserve text parts and provider options when serializing prompts. + +- [#7639](https://github.com/Effect-TS/effect/pull/7639) [`58be972`](https://github.com/Effect-TS/effect/commit/58be97238ee1f6cd26f6a3a645d1df97a091a224) Thanks @kitlangton! - Preserve generated files when converting AI responses to prompts. + +- [#7603](https://github.com/Effect-TS/effect/pull/7603) [`1320075`](https://github.com/Effect-TS/effect/commit/1320075fd4ed381f60f6f37f280dbec17cb29b81) Thanks @kitlangton! - Fix capacity-one PubSub subscriber cursors after sliding past messages, including duplicate delivery and invalid state when unsubscribing from a slid message. + +- [#7487](https://github.com/Effect-TS/effect/pull/7487) [`ba53b64`](https://github.com/Effect-TS/effect/commit/ba53b646e9dad9b39fd6cf5b2d89de9bf858bb80) Thanks @tim-smart! - Redesign `Socket` around a scoped, pull-based reader with transport backpressure. + + `Socket` now exposes `reader` and `writer`. Client reader acquisition dials and yields a pull of non-empty batches: one buffer for TCP and one entry per WebSocket frame. TCP applies backpressure while paused; pausable WebSockets pause at `highWaterMark` (64 KiB by default) and resume after draining. Browser WebSockets cannot pause, so they can fail with `SocketReadError` at a configured `highWaterMark`. Writes await native drain signals and batch with `cork` / `uncork` where available. + + ### Breaking changes + + - `Socket.run`, `Socket.runString`, and `Socket.runRaw` are removed. Acquire `socket.reader` (or `Socket.readerBytes` / `Socket.readerString`) in a scope and pull in a loop. Code before the first pull replaces `onOpen`. + - `Socket.make` now takes `{ reader, writer }`. The writer acquisition is infallible and yields a `Writer` with `write` and `writeAll`; both operations can still fail with `SocketError`. + - Every close fails the pull with `SocketError` wrapping `SocketCloseError`. The close-code predicates are removed; use `Effect.retry` around the scoped read loop to reconnect. + - `Socket.toChannel` and `Socket.toChannelString` now read from the pull and fail on close. `Socket.toStream` is added for read-only consumption. + - `fromWebSocket` drops the `onInitialRun` option; `SendQueueCapacity` is removed. + - Accepted server sockets pause immediately. Their reader attaches to the existing connection and cannot reconnect after close. + +- [#7806](https://github.com/Effect-TS/effect/pull/7806) [`f7490d4`](https://github.com/Effect-TS/effect/commit/f7490d49c75b99118c6c42549511ffa166459f59) Thanks @tim-smart! - Add `Queue.flush` and `Queue.flushUnsafe` for manually releasing pending takers, including after synchronous offers. + +- [#7576](https://github.com/Effect-TS/effect/pull/7576) [`c8ea602`](https://github.com/Effect-TS/effect/commit/c8ea60231ad69e23c0bf319f6f6b3ff28fc45e4b) Thanks @kitlangton! - Fix `Queue` message duplication, capacity overruns, and consumer defects when a resumed producer synchronously uses the same queue. Zero-capacity queues now reserve each handed-off message for its consumer before resuming the producer. + +- [#7498](https://github.com/Effect-TS/effect/pull/7498) [`62d82f4`](https://github.com/Effect-TS/effect/commit/62d82f4919c72d8cc0cfd2b704ed6900f69ad9a8) Thanks @gjermundgaraba! - Allow `SqlEventJournal` to decode entry identifiers and payloads from SQL drivers that return BLOB values as `ArrayBuffer`. + +- [#7644](https://github.com/Effect-TS/effect/pull/7644) [`a2c9e7c`](https://github.com/Effect-TS/effect/commit/a2c9e7c17a4118b74a1eab3332042dafe0bb7318) Thanks @gcanti! - Remove the redundant `Graph.Proto` interface. Use `Graph.Graph` when accepting any immutable graph. + +- [#7497](https://github.com/Effect-TS/effect/pull/7497) [`97dd022`](https://github.com/Effect-TS/effect/commit/97dd022fe7fbd1696dd60f3319eae594abdd3760) Thanks @javascript-unsafe! - Treat `NaN` as a non-positive count in `Stream.take`. + +- [#7864](https://github.com/Effect-TS/effect/pull/7864) [`f984ee8`](https://github.com/Effect-TS/effect/commit/f984ee82d255c7958eeed6f0dd08de1b88cbbfd6) Thanks @kitlangton! - Fix `Random.nextBetween` and `Crypto.randomBetween` returning their exclusive upper bound when floating-point arithmetic rounds up. + +- [#7985](https://github.com/Effect-TS/effect/pull/7985) [`f1b2910`](https://github.com/Effect-TS/effect/commit/f1b291027c3055f801c2f288dda21ba84c361956) Thanks @kitlangton! - Report the exact remaining store lifetime in `RateLimiter` fixed-window `resetAfter` metadata when `onExceeded` is `"delay"`, instead of rounding up to a whole window. Admission, returned delays, and remaining-token counts are unchanged. + +- [#7516](https://github.com/Effect-TS/effect/pull/7516) [`a29e05a`](https://github.com/Effect-TS/effect/commit/a29e05a907053a8617763b4e7408e1f012b59049) Thanks @kitlangton! - Fix `RcMap` and `LayerMap` cleanup after invalidating an actively borrowed entry and reacquiring the same key. + The invalidated resource is released when its last borrower closes, even with infinite idle TTL, without removing + the replacement entry. Old idle timers also leave replacement entries untouched. + +- [#7605](https://github.com/Effect-TS/effect/pull/7605) [`1aa1d8b`](https://github.com/Effect-TS/effect/commit/1aa1d8bd0f2c2d429938657927f34f8ff0f940a0) Thanks @kitlangton! - Fix `RcMap` entries getting stuck when the lookup function throws synchronously. Later borrowers now receive the defect, and unused entries are released according to their idle TTL instead of permanently consuming capacity. + +- [#7598](https://github.com/Effect-TS/effect/pull/7598) [`bb99734`](https://github.com/Effect-TS/effect/commit/bb99734ef87bcc377c628bc4072aa856ce5f7ca6) Thanks @kitlangton! - Keep `RcRef` closed when an in-flight acquisition finishes after its owning scope has closed. Release the late-acquired resource and interrupt waiting borrowers instead of making the resource available again. + +- [#7586](https://github.com/Effect-TS/effect/pull/7586) [`222e7ca`](https://github.com/Effect-TS/effect/commit/222e7caa4bb0da6d81a4eabf25dffc181c20f6a3) Thanks @kitlangton! - Prevent `RcRef` borrower cleanup from discarding replacement resources after invalidation or reopening a reference after its owner scope has closed. + +- [#7565](https://github.com/Effect-TS/effect/pull/7565) [`797c9e3`](https://github.com/Effect-TS/effect/commit/797c9e3c1f2e66db1570cf6aa5c4a15e94507bf0) Thanks @typedrat! - Type `Cause.Reason#annotate` as accepting a `Context` only. + +- [#7461](https://github.com/Effect-TS/effect/pull/7461) [`b4d5398`](https://github.com/Effect-TS/effect/commit/b4d5398598c04a84054a55873c943d587880058d) Thanks @tim-smart! - Remove the MessagePack encoding and RPC serialization APIs together with the `msgpackr` dependency. Event-log persistence and remote messages now use SchemaBinary, and cluster transports use SchemaBinary unless NDJSON is selected explicitly. + +- [#8143](https://github.com/Effect-TS/effect/pull/8143) [`c8349ed`](https://github.com/Effect-TS/effect/commit/c8349ede1a2add807e9ec9c37612b3a39bf284f8) Thanks @gcanti! - Rename `SchemaGetter.transformOrFail` to `SchemaGetter.transformEffect` and `SchemaTransformation.transformOrFail` to `SchemaTransformation.transformEffect`. Replace calls to the old names with their `transformEffect` equivalents. + +- [#8022](https://github.com/Effect-TS/effect/pull/8022) [`8426e5f`](https://github.com/Effect-TS/effect/commit/8426e5f7ad11f15e39565cf8ba0874bf974f5085) Thanks @kitlangton! - Correct the `Effect.repeatOrElse` fallback type to expose the previous step's `Schedule.Metadata`, matching the existing runtime value. + +- [#7520](https://github.com/Effect-TS/effect/pull/7520) [`26e0085`](https://github.com/Effect-TS/effect/commit/26e0085d098f6e526c085c357b8d82ee068b6518) Thanks @ebramanti! - Report recovered MCP toolkit failures and defects to configured `ErrorReporter`s, including declared tool failures returned with `isError: true`. + +- [#7583](https://github.com/Effect-TS/effect/pull/7583) [`1a86166`](https://github.com/Effect-TS/effect/commit/1a86166f102b517ee4d89dca0095e28f738eee10) Thanks @kitlangton! - Fix `RequestResolver.withCache` retaining abandoned entries when a pending request is cancelled + +- [#7613](https://github.com/Effect-TS/effect/pull/7613) [`0856631`](https://github.com/Effect-TS/effect/commit/0856631a89d8f72d206c366905ddac75468a8e85) Thanks @kitlangton! - Preserve completed results and propagate resolver failures from `RequestResolver.persisted`. + +- [#7594](https://github.com/Effect-TS/effect/pull/7594) [`0af0985`](https://github.com/Effect-TS/effect/commit/0af0985d5c132b33e14a2df59e0567d7ba2a1710) Thanks @kitlangton! - Keep completed results in `RequestResolver.withCache` when a losing `RequestResolver.race` resolver is interrupted after the winner completes, avoiding repeated backend requests on subsequent equal lookups. + +- [#7979](https://github.com/Effect-TS/effect/pull/7979) [`bc582c9`](https://github.com/Effect-TS/effect/commit/bc582c94313cbb89d87b3844ffffabebbb4ffa83) Thanks @kitlangton! - Preserve typed errors, defects, and interrupts from `RequestResolver.fromEffectTagged` handlers. + +- [#7981](https://github.com/Effect-TS/effect/pull/7981) [`2c63f1e`](https://github.com/Effect-TS/effect/commit/2c63f1e07e375b7958e14929c6a4b358c819a9d0) Thanks @kitlangton! - Fix `RequestResolver.fromEffectTagged` to consume handler results as an iterable, allowing arrays, iterators, and generators to resolve requests in order. + +- [#8024](https://github.com/Effect-TS/effect/pull/8024) [`0d98213`](https://github.com/Effect-TS/effect/commit/0d9821346453d44696258103643d5482e63638b4) Thanks @kitlangton! - Fix `Types.RequiredKeys` dropping named required keys on types with index signatures. Derived type annotations may need to include these keys. + +- [#7496](https://github.com/Effect-TS/effect/pull/7496) [`ca6f0dc`](https://github.com/Effect-TS/effect/commit/ca6f0dc4fcea564b351bf505df015d16a8b965ed) Thanks @nikhilsnayak! - Add `HttpClientResponse.url`, including query parameters and excluding the hash. When redirects are followed, it reports + the final URL. + +- [#7683](https://github.com/Effect-TS/effect/pull/7683) [`d8cc9ed`](https://github.com/Effect-TS/effect/commit/d8cc9ed80e1df9c056a46ba62df01f7fc0f531c3) Thanks @kitlangton! - Preserve zero and empty-string request IDs in JSON-RPC control messages. + +- [#7930](https://github.com/Effect-TS/effect/pull/7930) [`42fd969`](https://github.com/Effect-TS/effect/commit/42fd969b45025eed6d60cb87b97c5ff88d187671) Thanks @tim-smart! - The default scheduler falls back to a microtask when setting a timer throws. Cloudflare Workers disallow timers in global scope, so an effect that yielded while running at module load failed with "Disallowed operation called within global scope". + +- [#7558](https://github.com/Effect-TS/effect/pull/7558) [`5641ad3`](https://github.com/Effect-TS/effect/commit/5641ad333a88bb9e56baf886006682678391be0a) Thanks @gcanti! - Move the built-in schema revivers from `Schema` to `SchemaRepresentation`. + Rename the reviver constructors to `makeReviverDeclaration`, + `makeReviverFilter`, and `makeReviverFilterGroup`. + + Change `Schema.toEncoderXml` to fail with `SchemaIssue.Issue` directly instead + of wrapping failures in `SchemaError`. Consumers that read `error.issue` should + now use the error value itself. + +- [#7641](https://github.com/Effect-TS/effect/pull/7641) [`629870d`](https://github.com/Effect-TS/effect/commit/629870d461b36d3dacb30c8d203b8d20d4beaba0) Thanks @kitlangton! - Preserve array-valued leaves when `SchemaGetter.makeTreeRecord` aggregates duplicate paths. + +- [#7673](https://github.com/Effect-TS/effect/pull/7673) [`d592c14`](https://github.com/Effect-TS/effect/commit/d592c1492ad895ea72537e10387b735f3ba0e7de) Thanks @kitlangton! - Preserve leading U+FEFF characters in SchemaBinary string values when decoding. + +- [#8131](https://github.com/Effect-TS/effect/pull/8131) [`10d2c98`](https://github.com/Effect-TS/effect/commit/10d2c983a80d19b84fb52f33c7f719ebd81746bb) Thanks @gcanti! - Align Schema construction and parsing semantics, simplify parse options, accept inherited declared fields, and move Union settings into a node-local options object. + + ### Breaking changes + + - `Class.make`, `Class.makeOption`, and `Class.makeEffect` now return an existing instance unchanged. This avoids duplicate initialization and makes the construction APIs consistent. Use `new MyClass(input)` when a distinct instance is required. + + - `Literal(0)` and `Literal(-0)` continue to accept either signed zero, but decoding and encoding now preserve the input sign. Add an explicit transformation when a canonical sign is required. + + - `parseOptions` annotations no longer affect parsing. Options passed when creating or calling a decoder, encoder, or constructor adapter now apply to the complete operation. Move operation-wide settings from annotations to the relevant parser API. + + - `propertyOrder` has been removed from `ParseOptions` because preserving input order required a separate, rarely used object reconstruction path. Schema parsing no longer guarantees that decoded object keys follow their input order. Remove the option and apply any required presentation or serialization order after parsing. + + - `concurrency` now applies only to product children: tuple elements, array elements, struct fields, record entries, and structs with rest. It follows `Effect.forEach` semantics, defaults to sequential execution, and applies independently at every nested product. Union candidates remain sequential because speculative candidate evaluation can run transformations that are not selected. Existing product parsing can keep the option. Replace code that relied on concurrent Union candidates with explicitly coordinated parser calls. With concurrent Record key transformations, completion order determines the retained value when transformed keys collide. + + - `onExcessProperty: "preserve"` has been removed because it allowed unvalidated values absent from the schema type to cross the parsing boundary. Model additional properties with `Record` or `StructWithRest`; `"ignore"` and `"error"` remain available. + + - Declared `Struct` fields may now be inherited and are copied to own properties in the output. Dynamic `Record` index signatures remain own-only, while finite literal record keys are declared and may be inherited. The `__proto__` field remains own-only. Check ownership before parsing when every declared field must be own. + + - `SchemaAST.Union.mode` moved to `SchemaAST.Union.options?.mode` so node-local constructor settings live in one options object instead of special top-level fields. An absent value defaults to `"anyOf"`. `SchemaRepresentation.Union` now serializes `{ options: { mode: "oneOf" } }`; update direct AST access and regenerate or migrate persisted representation documents. The public `Schema.Union(members, { mode })` call is unchanged. + +- [#8147](https://github.com/Effect-TS/effect/pull/8147) [`53909a9`](https://github.com/Effect-TS/effect/commit/53909a9bf14b29725a08dcccde02bee8e95a6b14) Thanks @gcanti! - JSON Schema generation now follows the canonical JSON codec more closely and leaves unmodeled object properties open by default, matching Effect decoding. + + ### Breaking changes + + `Schema.ToJsonSchemaOptions.additionalProperties` has been replaced by `onExcessProperty`: + + - Replace `{ additionalProperties: true }` with `{ onExcessProperty: "ignore" }`. + - Replace `{ additionalProperties: false }` with `{ onExcessProperty: "error" }`. + - Replace a schema-valued `additionalProperties` option with `Schema.Record` or `Schema.StructWithRest`. + + `Schema.Enum` now rejects non-finite numeric members. `Schema.isMultipleOf` now rejects zero and non-finite divisors, and normalizes negative divisors. + + Generation is more accurate for index signatures, empty structs, template literal alternatives, capitalized strings, and unique symbols. Conjunctive index keys remain open by default; `onExcessProperty: "error"` constrains them with `propertyNames`. + +- [#7606](https://github.com/Effect-TS/effect/pull/7606) [`78a4269`](https://github.com/Effect-TS/effect/commit/78a42697f6085c87c09b18e8095bb7541bd65261) Thanks @kitlangton! - Fix `ScopedCache.invalidateAll` discarding entries created by reentrant resource finalizers without releasing them. Entries are now removed before their finalizers run, so replacement resources remain cached and are released when the cache closes. + +- [#7612](https://github.com/Effect-TS/effect/pull/7612) [`06c6307`](https://github.com/Effect-TS/effect/commit/06c6307d3d5538bc398c67a44239bc3d52ab1a4d) Thanks @kitlangton! - Capture synchronous defects thrown by `ScopedCache.refresh` lookup callbacks. + +- [#8026](https://github.com/Effect-TS/effect/pull/8026) [`0847c41`](https://github.com/Effect-TS/effect/commit/0847c41e2472a347cd8f77d460d3cbdfecd4adf0) Thanks @kitlangton! - Fix `Effect.annotateLogsScoped` to restore or remove unchanged `NaN` annotations when the scope closes. + +- [#8155](https://github.com/Effect-TS/effect/pull/8155) [`5a77084`](https://github.com/Effect-TS/effect/commit/5a77084651bc58617b9988606473cf9ba5ca0032) Thanks @tim-smart! - Use branded interfaces for `Reactivity`, `LanguageModel`, `EmbeddingModel`, and `Chat`. Refer to each service's same-name type instead of `.Service` or `["Service"]`; custom implementations must include `[TypeId]: TypeId`. + +- [#7913](https://github.com/Effect-TS/effect/pull/7913) [`96f99b3`](https://github.com/Effect-TS/effect/commit/96f99b325da32bfa535e826d89d838d0edec0dea) Thanks @Hoishin! - Fix HttpRouter nested prefixed application order + +- [#7906](https://github.com/Effect-TS/effect/pull/7906) [`7bb8781`](https://github.com/Effect-TS/effect/commit/7bb878176b8ad20cf0e671b570caf72e194f9515) Thanks @kitlangton! - Honor services explicitly supplied when registering cluster entities while retaining construction-context services as fallbacks. + +- [#8114](https://github.com/Effect-TS/effect/pull/8114) [`4907e9b`](https://github.com/Effect-TS/effect/commit/4907e9b2470894ddaf0cbdec2612b59f04a51dbe) Thanks @tim-smart! - Skip optional stack capture when `Error.stackTraceLimit` is zero. + +- [#8090](https://github.com/Effect-TS/effect/pull/8090) [`2a30248`](https://github.com/Effect-TS/effect/commit/2a30248b6eb739f22403456209bc468f2f4ef26a) Thanks @tim-smart! - Reduce the basic Effect bundle size by keeping cause deduplication local, making encoding lookup tables tree-shakeable, removing redundant cause field declarations, and simplifying primitive hash dispatch without changing hash values. + +- [#7825](https://github.com/Effect-TS/effect/pull/7825) [`8364ddd`](https://github.com/Effect-TS/effect/commit/8364ddda233d35ee472cc2356312317a60a148a2) Thanks @kitlangton! - Resume paused WebSockets after their readers take ownership. + +- [#7827](https://github.com/Effect-TS/effect/pull/7827) [`ad67d8c`](https://github.com/Effect-TS/effect/commit/ad67d8c5a1b195ff5468a9f2e963c59aa0b3166f) Thanks @kitlangton! - Count buffered WebSocket text frames by their UTF-8 byte length when enforcing `highWaterMark`. + +- [#7798](https://github.com/Effect-TS/effect/pull/7798) [`ec0c087`](https://github.com/Effect-TS/effect/commit/ec0c0875262fb81c7992f5023776b8fa894c49b6) Thanks @kitlangton! - Emit CR-terminated lines from `Stream.splitLines` without pulling upstream again. + +- [#7443](https://github.com/Effect-TS/effect/pull/7443) [`fa6a56b`](https://github.com/Effect-TS/effect/commit/fa6a56b862229cfb699e076bac50e3b737ae3c72) Thanks @youngspe! - Loosen Stream.addEventListener type parameter + +- [#7844](https://github.com/Effect-TS/effect/pull/7844) [`a2c1ce6`](https://github.com/Effect-TS/effect/commit/a2c1ce62f04214d98d848f4cfe33d677e1a87d39) Thanks @kitlangton! - Preserve callback error identity in `SqlEventJournal.write` and `SqlEventJournal.withRemoteUncommited`. + +- [#7837](https://github.com/Effect-TS/effect/pull/7837) [`91e9af0`](https://github.com/Effect-TS/effect/commit/91e9af0d5f138933d3a131ca1cfbc52d3bc55481) Thanks @kitlangton! - Preserve reply IDs in SQL-backed `MessageStorage.unprocessedMessagesById` reads. + +- [#7635](https://github.com/Effect-TS/effect/pull/7635) [`7bd3f34`](https://github.com/Effect-TS/effect/commit/7bd3f34385a06157b249a351e32ffdcbe1cf2c37) Thanks @kitlangton! - Fix placeholder numbering for cached fragments used in returning helpers. + +- [#7493](https://github.com/Effect-TS/effect/pull/7493) [`ef16581`](https://github.com/Effect-TS/effect/commit/ef165814eae0a199fdca5b3c2839bb4fcdfd5b5d) Thanks @utopyin! - Add `Statement.SpanPropagationEnabled` to scope driver span parenting under `sql.execute` for any SQL client. Disabled by default. + + ```ts + import { Effect } from "effect" + import { Statement } from "effect/unstable/sql" + + query.pipe(Effect.provideService(Statement.SpanPropagationEnabled, true)) + ``` + +- [#7633](https://github.com/Effect-TS/effect/pull/7633) [`1693a87`](https://github.com/Effect-TS/effect/commit/1693a877324273f5a23fa7d7fc892f4ee12f20c2) Thanks @kitlangton! - Fix SQL returning helpers to compile identifiers with dialect-specific escaping. + +- [#7860](https://github.com/Effect-TS/effect/pull/7860) [`df3fc47`](https://github.com/Effect-TS/effect/commit/df3fc47609eda7843dae2b557c8fe834299e26b4) Thanks @kitlangton! - Ensure PostgreSQL shard acquisition and refresh return only the requested shards. + +- [#7904](https://github.com/Effect-TS/effect/pull/7904) [`e11be41`](https://github.com/Effect-TS/effect/commit/e11be41dfc73fa6b5643ab9af7df18d6907927d7) Thanks @kitlangton! - Include the model's decoding services in the public requirements of `SqlModel.makeResolvers().insert`, alongside its existing input-encoding services. + + This intentionally tightens compile-time checking: previously accepted callers must now provide the services already needed to decode inserted rows at runtime. Provide those services when executing the insert with `SqlResolver.request`. `insertVoid` still requires only input-encoding services, and service-free models need no changes. Runtime behavior is unchanged. + +- [#7655](https://github.com/Effect-TS/effect/pull/7655) [`2e39e8b`](https://github.com/Effect-TS/effect/commit/2e39e8bafef7d3fee9f3b324b7d50cc064376667) Thanks @kitlangton! - Fix `Stream.rechunk` failing on large source chunks. + +- [#7538](https://github.com/Effect-TS/effect/pull/7538) [`11c5ee7`](https://github.com/Effect-TS/effect/commit/11c5ee7202acd3bb789d0a8ce78e6c4523071553) Thanks @gwagjiug! - Parse `Content-Length` metadata strictly across HTTP modules, ignoring malformed or unsafe values instead of coercing them. + +- [#7522](https://github.com/Effect-TS/effect/pull/7522) [`1742d2f`](https://github.com/Effect-TS/effect/commit/1742d2f48441cb7b1a8d9aca3a8e6f0acbc35a96) Thanks @gwagjiug! - Ignore `Set-Cookie` headers whose cookie names do not satisfy the RFC 6265 token syntax. + +- [#7619](https://github.com/Effect-TS/effect/pull/7619) [`8efc70e`](https://github.com/Effect-TS/effect/commit/8efc70ea16bee43fd9adde2871cf82a8131b80a0) Thanks @kitlangton! - Honor numeric property selectors in Struct selection and mapping utilities. + +- [#7560](https://github.com/Effect-TS/effect/pull/7560) [`9642776`](https://github.com/Effect-TS/effect/commit/964277661423f398f777ec2eab4b3ecc0b53f53b) Thanks @gcanti! - Expose `SchemaAST` nodes, `SchemaIssue` nodes, `SchemaGetter.Getter`, and the `SchemaTransformation` models through structural instance interfaces instead of concrete class declarations. The constructors remain usable with `new` and `instanceof`, but their `prototype` is no longer part of the public TypeScript API. Replace type-level access through a constructor's `prototype` with the corresponding named instance interface, such as `SchemaGetter.Getter`. + + `SchemaAST.Base` is no longer exported. Use `SchemaAST.AST` when accepting any AST node, and use the `SchemaAST.is*` guards to narrow individual variants. + +- [#7790](https://github.com/Effect-TS/effect/pull/7790) [`6680828`](https://github.com/Effect-TS/effect/commit/668082885f52672e4c7fad4e0fe2bfa0e69703e2) Thanks @kitlangton! - Fix the curried `SynchronizedRef.modifySomeEffect` overload to accept only the callback, matching its runtime behavior. + +- [#7569](https://github.com/Effect-TS/effect/pull/7569) [`c34edcb`](https://github.com/Effect-TS/effect/commit/c34edcb6d7a6151d53e19828c92abf7dd8450c2a) Thanks @tim-smart! - Stop declaring `SynchronizedRef` as a subtype of `Ref`, preventing `Ref` combinators from accepting values that do not implement the required runtime representation. + +- [#8012](https://github.com/Effect-TS/effect/pull/8012) [`7b2c5bd`](https://github.com/Effect-TS/effect/commit/7b2c5bd3c394fa4751fb229394ac9c03e401f441) Thanks @kitlangton! - Preserve the source error type when a saved `Effect.tapDefect` operator is applied. The source error is now inferred from each application instead of when the operator is created. Runtime behavior is unchanged. + +- [#7427](https://github.com/Effect-TS/effect/pull/7427) [`1a2ccee`](https://github.com/Effect-TS/effect/commit/1a2ccee2bbb93514dc66e0a9cdeb52a82172ab0e) Thanks @tim-smart! - Use SchemaBinary as the default RPC serialization for TCP cluster connections, including configurable frame limits. + + Cluster payloads are encoded with the binary codec on the wire. When a persisted reply cannot be encoded for JSON storage, the defect fallback that storage records is now also the reply delivered to waiting callers, so live replies always match what was persisted. + + SchemaBinary codecs are memoized by schema identity and wire mode, so per-message codec requests reuse the derived codec instead of rebuilding it. + +- [#8094](https://github.com/Effect-TS/effect/pull/8094) [`db995df`](https://github.com/Effect-TS/effect/commit/db995df19b0381b64cc39b50aa28d7bd8f2c1294) Thanks @gcanti! - Separate template literal validation from transformed tuple parsing. `TemplateLiteralParser` now propagates its parts' decoding and encoding service requirements. + + ### Breaking changes + + `Schema.TemplateLiteral` and `SchemaAST.TemplateLiteral` now throw during construction when a part contains an encoding, including inside unions and nested templates. This also rejects transformations whose decoded and encoded types are equal. Brands and supported checks without encodings remain valid. + + Use `Schema.Literals([0, 1])` to describe bit spellings or `Schema.Finite` to describe finite numeric spellings. Use `Schema.TemplateLiteralParser` when you need to decode transformed parts into a tuple. Explicit `Schema.toType` or `Schema.toEncoded` projections can remove an encoding, but do not necessarily preserve the strings accepted by the old template. For example, a `Finite` part rejects the empty segment accepted by `FiniteFromString`. + + `Schema.toEncoded(Schema.TemplateLiteralParser(...))` now validates the structure of the template instead of accepting any string. Use `Schema.String` when unrestricted strings are intended. + + When parser parts require services, provide those services to the corresponding decoding or encoding effect. These requirements were previously omitted from the parser's types. + +- [#7870](https://github.com/Effect-TS/effect/pull/7870) [`af0ccdd`](https://github.com/Effect-TS/effect/commit/af0ccdd13aa8917bce7161b8bc257fbe51c6a6a5) Thanks @kitlangton! - Fix `TestSchema.Asserts.ast.fields.equals` to compare ASTs for all own struct fields, including symbol and non-enumerable keys. Equivalent field schemas now compare equally regardless of schema instance identity, while differing ASTs and distinct symbol keys remain unequal. + +- [#8127](https://github.com/Effect-TS/effect/pull/8127) [`addeaea`](https://github.com/Effect-TS/effect/commit/addeaea0b0abe0dc24ca1d4d0fd194a0e458be70) Thanks @tim-smart! - dispatch websocket events directly + +- [#7448](https://github.com/Effect-TS/effect/pull/7448) [`7704034`](https://github.com/Effect-TS/effect/commit/770403411bcd8befef0552a0e40066abc417f6ea) Thanks @candrewlee14! - Fix response tool part assignability after narrowing generic intersected tool records. + +- [#7486](https://github.com/Effect-TS/effect/pull/7486) [`e72b12f`](https://github.com/Effect-TS/effect/commit/e72b12fc305710550bc6dcb978e92de8abff88cd) Thanks @tim-smart! - Make automatic tool resolution interruption-safe for incomplete language model responses. + +- [#7481](https://github.com/Effect-TS/effect/pull/7481) [`310dd9c`](https://github.com/Effect-TS/effect/commit/310dd9ce9681e97f558bb042adb35f6040ab81e3) Thanks @jpenilla! - Restore the `Effect.timeout` error message so `TimeoutError` includes the elapsed duration. + +- [#8032](https://github.com/Effect-TS/effect/pull/8032) [`1c2afc1`](https://github.com/Effect-TS/effect/commit/1c2afc15bbf4ce5a91d24da99bc275b5bd744e77) Thanks @kitlangton! - Fix `Effect.timeoutOrElse` to finish interrupting the source before evaluating the fallback, preventing the source from winning after the timeout. + + Fallbacks now run in the caller fiber and inherit its interruptibility and supervision. + +- [#8103](https://github.com/Effect-TS/effect/pull/8103) [`44f44ca`](https://github.com/Effect-TS/effect/commit/44f44cae65249801c082c3a3848016eeeac5eb24) Thanks @tim-smart! - Fix token-bucket `retryAfter`, `delay` and `resetAfter` in the memory and Redis stores. Timing now follows whole-token refill boundaries and accounts for elapsed time, including fractional token costs. Redis preserves signed fractional counts and keeps keys until capacity actually refills. + + `RateLimiterStore.tokenBucket` now returns `[remaining, elapsedMillis]` instead of `remaining`. Custom stores must return both values from the same atomic operation; see the `tokenBucket` docs for the contract. Returning `[remaining, 0]` keeps the old timing bug. + +- [#7912](https://github.com/Effect-TS/effect/pull/7912) [`f43b9d6`](https://github.com/Effect-TS/effect/commit/f43b9d6d99cb591c0377bb1474121827201627c9) Thanks @kitlangton! - Fix `Tokenizer.truncate` to account for token costs between messages. + +- [#7748](https://github.com/Effect-TS/effect/pull/7748) [`56e72b3`](https://github.com/Effect-TS/effect/commit/56e72b38c01bbe6ec921ad151f83b7849444f219) Thanks @kitlangton! - Encode tool results with the schema for their known success or failure branch. + +- [#8030](https://github.com/Effect-TS/effect/pull/8030) [`50ef80e`](https://github.com/Effect-TS/effect/commit/50ef80e251b85b5ee63aed06fe0244b5433ec2fa) Thanks @kitlangton! - Fix `Effect.track(metric, mapper)` to reject source errors the mapper cannot handle. + +- [#7774](https://github.com/Effect-TS/effect/pull/7774) [`fc3b718`](https://github.com/Effect-TS/effect/commit/fc3b7187114a31cbb21a250f55fda601bb3edcf0) Thanks @kitlangton! - Preserve valued prefix nodes when removing a longer key from a `Trie`. + +- [#8016](https://github.com/Effect-TS/effect/pull/8016) [`ee336d8`](https://github.com/Effect-TS/effect/commit/ee336d80b4024b51654e7a2b8d869d244153fcdf) Thanks @kitlangton! - Correct the error types of `Effect.try` and `Effect.tryPromise`. Direct function forms retain `Cause.UnknownError`, while `{ try, catch }` options use the error type returned by `catch`. + + Explicit two-generic direct calls, union-valued arguments, and generic aliases that combine the two forms no longer compile. Use `{ try, catch }` with a real error mapper, or narrow a union before calling the constructor. + + Runtime behavior, callback arguments, and error mapping are unchanged. + +- [#7924](https://github.com/Effect-TS/effect/pull/7924) [`d12f922`](https://github.com/Effect-TS/effect/commit/d12f922853eff2310dbe9fc9b31562ad81b38435) Thanks @kitlangton! - Correct `Tuple.evolve` result types when a transform may be `undefined`. The result now includes both the transformed and unchanged element types, matching the existing runtime behavior. Accepted inputs and runtime behavior are unchanged. + + Code relying on the previous, incorrect result type must handle both outcomes. For example, a number-to-string transform that may be absent now produces `number | string`, so callers assuming a number-only result must adjust. + +- [#7553](https://github.com/Effect-TS/effect/pull/7553) [`46d8310`](https://github.com/Effect-TS/effect/commit/46d83101e8408c0428d9a8760e06e23a69ad6070) Thanks @tim-smart! - Move the Cookie, Cookies, Headers, and UrlParams schemas from `effect/unstable/http` to `effect/Schema`, including their record and JSON-field helper schemas. + +- [#7623](https://github.com/Effect-TS/effect/pull/7623) [`59812fd`](https://github.com/Effect-TS/effect/commit/59812fd221cde16fc77c03d326c6a2cf8fbec6b9) Thanks @kitlangton! - Fix `UrlParams.fromInput` to stringify `null` values. + +- [#7631](https://github.com/Effect-TS/effect/pull/7631) [`f9d0dec`](https://github.com/Effect-TS/effect/commit/f9d0dec12c5f229f05d7a74d532a85c6bbffefbf) Thanks @kitlangton! - Prevent `UrlParams.setAll` from mutating reusable overrides. + +- [#7855](https://github.com/Effect-TS/effect/pull/7855) [`4372c79`](https://github.com/Effect-TS/effect/commit/4372c79a32fcecc7b6b284d49a215b2c4fef9d77) Thanks @gcanti! - Treat only unpadded decimal integers from `0` through `4294967294` as array indices in environment-backed configuration and bracket-path decoding. This preserves numeric-looking object keys and prevents out-of-range environment keys from producing impossible array lengths. Bracket paths that intend to address arrays must use `[1]` instead of `[01]`. + +- [#7551](https://github.com/Effect-TS/effect/pull/7551) [`81485ef`](https://github.com/Effect-TS/effect/commit/81485ef0288a3321e75d7d4ae32b7e0e06b6f86b) Thanks @tim-smart! - Cluster shard-lock recovery no longer stalls behind a wedged reserved SQL connection. + + While lock storage is unhealthy, the empty liveness probe (`refresh(address, [])`) now runs on the shared pool instead of the reserved lock connection, so a hung reserved connection cannot block recovery. Failed probes are also logged as warnings instead of being silently swallowed. + +- [#8028](https://github.com/Effect-TS/effect/pull/8028) [`bd393d6`](https://github.com/Effect-TS/effect/commit/bd393d63c19bdd0ab212d95576cec89051c8501c) Thanks @kitlangton! - Fix `Effect.withErrorReporting` to return an `Effect` instead of preserving input + subtypes such as `Exit`, whose subtype-specific fields are not present on the wrapper. + +- [#7570](https://github.com/Effect-TS/effect/pull/7570) [`0c95c04`](https://github.com/Effect-TS/effect/commit/0c95c04b41c2d5d09e4564fd997572207fd72595) Thanks @tim-smart! - Fix `Worker.run` hanging uninterruptibly when a worker dies before the ready handshake. + ## 4.0.0-rc.112 ### Minor Changes diff --git a/repos/effect/packages/effect/MCP.md b/repos/effect/packages/effect/MCP.md index 66258490c..53f77d24f 100644 --- a/repos/effect/packages/effect/MCP.md +++ b/repos/effect/packages/effect/MCP.md @@ -9,17 +9,16 @@ It's important to understand the architecture of the Effect MCP server. Here is an example of a MCP server implementation: ```typescript -import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node" -import { Effect, Layer, Logger } from "effect" -import { Schema } from "effect/schema" +import { NodeRuntime, NodeStdio } from "@effect/platform-node" +import { Effect, Layer, Logger, Schema } from "effect" import { McpProtocol, McpServer, Tool, Toolkit } from "effect/unstable/ai" // Define a simple tool const DemoTool = Tool.make("DemoTool", { description: "A demo tool that echoes back the input", - parameters: { + parameters: Schema.Struct({ message: Schema.String - }, + }), success: Schema.String }) @@ -58,12 +57,12 @@ 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 + protocols: [McpProtocol.v2025_06_18] }) ), - Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])) + Layer.provide(NodeStdio.layer), + Layer.provide(Logger.layer([Logger.consolePretty()])), + Layer.provideMerge(Layer.succeed(Logger.LogToStderr, true)) ) Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) @@ -99,8 +98,7 @@ resource is defined as a template that specifies its location, behavior, and met parameters, completions, and content generation. ```typescript -import { Effect } from "effect" -import { Schema } from "effect/schema" +import { Effect, Schema } from "effect" import { McpSchema, McpServer } from "effect/unstable/ai" const SimpleResource = McpServer.resource({ @@ -141,8 +139,7 @@ structured, parameterized instructions or messages that the client can send to t generation logic in a declarative way. ```typescript -import { Effect } from "effect" -import { Schema } from "effect/schema" +import { Effect, Schema } from "effect" import { McpServer } from "effect/unstable/ai" const DemoPrompt = McpServer.prompt({ @@ -169,24 +166,23 @@ contract while the actual logic is provided separately through an implementation grouped into toolkits, which can be combined and converted into layers. ```typescript -import { Effect, Layer } from "effect" -import { Schema } from "effect/schema" +import { Effect, Layer, Schema } from "effect" import { McpServer, Tool, Toolkit } from "effect/unstable/ai" const DemoTool = Tool.make("DemoTool", { description: "This is a demo tool for the documentation", - parameters: { + parameters: Schema.Struct({ demoId: Schema.Number, demoName: Schema.String - }, + }), success: Schema.String }) const OtherDemoTool = Tool.make("OtherDemoTool", { description: "Another demo tool", - parameters: { + parameters: Schema.Struct({ value: Schema.Number - }, + }), success: Schema.String }) @@ -217,8 +213,7 @@ defines both the message shown to the user and the expected response schema, ens validated user input. ```typescript -import { Effect } from "effect" -import { Schema } from "effect/schema" +import { Effect, Schema } from "effect" import { McpServer } from "effect/unstable/ai" const DemoElicitation = McpServer.elicit({ @@ -369,7 +364,7 @@ const ServerLayer = Layer.mergeAll( }) ), Layer.provide(NodeStdio.layer), - Layer.provide(Layer.succeed(Logger.LogToStderr)(true)) + Layer.provideMerge(Layer.succeed(Logger.LogToStderr, true)) ) // Run the server diff --git a/repos/effect/packages/effect/SCHEMA.md b/repos/effect/packages/effect/SCHEMA.md index 3812de314..98e96898d 100644 --- a/repos/effect/packages/effect/SCHEMA.md +++ b/repos/effect/packages/effect/SCHEMA.md @@ -35,7 +35,7 @@ Use Schema to: 11. **Middlewares** — intercept decoding/encoding to provide fallbacks or inject services. 12. **Advanced topics** — internal type model and type hierarchy (for library authors). 13. **Integrations** — working examples for TanStack Form and Elysia. -14. **Migration from v3** — API mapping from Schema v3 to v4. +14. **[Migration from v3](../../migration/schema.md)** — API mapping from Schema v3 to v4. ## Runtime Performance @@ -51,24 +51,151 @@ 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** | — | — | +| Scenario | Effect Schema | Valibot 1.5.0 | Zod 4.6.2 | +| ------------------------------------- | ------------: | ------------: | --------: | +| Create a schema | 60.1297 | 1.0926 | 83.7496 | +| Validate valid data | 4.1473 | 3.5389 | — | +| Validate invalid data | 0.2571 | 0.1823 | — | +| Parse valid data and collect errors | 5.0261 | 3.5393 | 7.0073 | +| Parse invalid data and collect errors | 7.4838 | 5.7206 | 19.3064 | +| Parse valid data and stop early | 4.1559 | 3.5941 | — | +| Parse invalid data and stop early | 0.2525 | 0.1917 | — | +| Standard Schema, valid data | **5.3408** | 3.5565 | 3.5755 | +| Standard Schema, invalid data | 11.7723 | 5.4422 | 15.9974 | +| Encode with a typed codec | 0.0827 | — | 0.0441 | +| Decode with a typed codec | 0.1063 | — | 0.0427 | + +## Experimental schema compilers + +Enable JIT compilation at application startup with a side-effect import: + +```ts +import "effect/unstable/schema/SchemaJITCompiler/enable" +``` + +Alternatively, `SchemaJITCompiler.enable(schema.ast)` enables one AST and the +dependencies reached while parsing it. Importing `SchemaJITCompiler` or the +`unstable/schema` barrel alone does not enable compilation. Operations are +prepared on first use. If dynamic function construction is blocked or compilation +fails, the interpreter remains available. Exceptions from executing a parser are +not treated as compilation failures and do not trigger a retry. + +JIT and AOT use the same source generator. To generate an AOT module at build +time, call `SchemaAOTCompiler.compile(targets)` with an ordered array of ASTs +and the operations to prepare: + +```ts +SchemaAOTCompiler.compile([ + { ast: User.ast, operations: ["decode"] }, + { ast: SchemaAST.toType(User.ast), operations: ["is", "make"] } +]) +``` + +The module exports `install(asts)`. Call it with the target ASTs in the same +order before using normal `SchemaParser` functions. Generated modules contain +only the requested operation families and their dependencies. Operations that +were not requested use the interpreter if they are called. Generated modules +do not import the generator and work where `new Function` is forbidden. + +The low-level installation trusts the supplied root order and AST definitions. +Target `SchemaAST.toType(schema.ast)` with `is` or `make` for guards and +construction. Target `SchemaAST.flip(schema.ast)` with `decode` for encoding. + +`effect/unstable/schema/SchemaAOTCompiler/Build` provides the higher-level +workflow. Its `build` function loads direct Schema exports, writes a +self-installing module through `FileSystem`, and prepares decoding by default: + +```ts +import * as SchemaAOTCompilerBuild from "effect/unstable/schema/SchemaAOTCompiler/Build" + +SchemaAOTCompilerBuild.build({ + modules: { + "./schemas/User.js": () => import("./schemas/User.js"), + "./schemas/Order.js": () => import("./schemas/Order.js") + }, + baseUrl: import.meta.url, + outFile: "./generated/schema-aot.js" +}) +``` + +Import the generated file at application startup. Module keys identify imports +relative to `baseUrl`; each loader must return that same module during the +build. The lazy record produced by `import.meta.glob` can be passed directly. +Request `encode`, `is`, or `make` explicitly when those directions also need +AOT roots. Loading executes the selected application modules during the build. +Run the returned Effect with the platform's `FileSystem` and `Path` services, +and ensure the bundler retains the generated side-effect import. + +Regenerate AOT modules when schema definitions or the Effect version change. +Callbacks and symbols are read from runtime ASTs, not serialized. + +### One registry for all implementations + +A single `WeakMap` associates each exact AST with its decoder entry. The cache +stores functions, never parsing results. The interpreter, JIT, AOT and +`SchemaCompiler.set(ast, decoder)` all use it. + +| Operation | Result | Purpose | +| -------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `decodeEffect` | `Effect` with output or detailed issues | Required complete decoding, including asynchronous work and transformations. | +| `decode` | Output or `SchemaCompiler.invalid` | Optional synchronous decoding fast path without detailed diagnostics. | +| `is` | Boolean | Optional validation without constructing output. | +| `make` | Output or `SchemaCompiler.invalid` | Optional synchronous construction fast path without detailed diagnostics. | +| `makeEffect` | `Effect` with a constructed value or issues | Optional specialized construction. The registry caches the interpreted constructor when absent. | + +Decoding tries `decode` when available. Success provides the output directly; +failure calls `decodeEffect` for diagnostics. The diagnostic traversal uses child +decoders directly, without restarting their validation fast paths. A boolean +guard prefers `is`, otherwise it uses ordinary decoding (including `decode` +when available). An `invalid` result needs that diagnostic fallback because the +marker is also a possible input value. Composite checks +can require stripped, reconstructed values, so `is` is omitted when it cannot +avoid constructing those values safely. + +Each operation initializes independently. Synchronous construction tries `make` +when available and falls back to `makeEffect` for detailed issues. Compilers omit +`make` whenever replay could repeat defaults, Class constructors, +transformations, middleware, or other effects. `makeEffect` itself never uses +validation replay. Field defaults belong to the parent occurrence, not to +construction of the root. Runtime parse options, including product concurrency, +retain the interpreter's semantics. + +Installing a decoder replaces the entry for that AST. Existing consumers that +already captured an entry retain it. Late installation is allowed, but startup +installation is needed to optimize every consumer. Custom decoders supplied to +`set` are trusted to implement the AST's semantics. + +An AOT module installs entries for explicitly requested roots and their static +dependencies. Each generated operation initializes on first use. All entries +live in the same `WeakMap`: there is no separate AOT cache, and a later +`SchemaCompiler.set` for the same AST replaces any compiled entry in the same way. + +### What is specialized + +Encoding-free graphs of supported primitives, Objects, Arrays, tuples, Unions +and template literals can use generated validators. Struct and homogeneous Array +decoding and construction also have generated loops. Pure fixed Struct and +homogeneous Array constructors can additionally use the synchronous `make` fast +path; composite children are resolved through the same registry. These loops +share the interpreter's diagnostic and asynchronous continuation helpers. +Other detailed traversals and constructors use the existing interpreter with +registry-resolved children; there is no separate diagnostic interpreter in the +compiler. + +Transformations and middleware never participate in validation replay. A single +synchronous transformation between supported leaf types can use generated +orchestration directly. Other transformations and middleware use the interpreted +orchestration, while pure child checkpoints can still use generated validators. +Suspend is resolved lazily by JIT. AOT does not evaluate Suspend thunks at build +time, so dynamically reached schemas fall back to the interpreter unless installed +separately. Declaration callbacks remain runtime code; their type parameters can +be compiled. + +Checks and property getters in replayable validation must be deterministic and +free of side effects. Proxy inputs and modifications to built-in object behavior +are not supported by the optimization contract. Large or unsupported graphs +retain interpreted paths. AOT removes dynamic source generation, not all parser +initialization or the need for runtime schema objects. # Defining Elementary Schemas @@ -286,6 +413,10 @@ You can use `Schema.TemplateLiteral` to define structured string patterns made o Template literal matching is based on the semantics of each part rather than only a generated regular expression. Checks on string, number, and bigint schema parts are applied while matching each segment. +Parts must not contain encodings. Construction throws for transformed parts, including transformations inside unions and transformations whose decoded and encoded types are equal. Brands and supported checks without encodings remain valid. Use `Schema.TemplateLiteralParser` when the parts need to decode values, such as `BooleanFromBit` or `FiniteFromString`. + +To describe bit spellings directly, use `Schema.Literals([0, 1])` as the part. To describe finite numeric spellings, use `Schema.Finite`. Replacing `FiniteFromString` with `Finite` changes the accepted spelling rules: a finite numeric part does not accept an empty segment. Explicit `Schema.toType` and `Schema.toEncoded` projections remove transformations, but can also change the constraints a template validates. + **Example** (Constraining parts of an email-like string) ```ts @@ -322,6 +453,12 @@ Failure(Cause([Fail(SchemaError(Expected a string matching template literal part If you want to extract the parts of a string that match a template, you can use `Schema.TemplateLiteralParser`. This allows you to parse the input into its individual components rather than treat it as a single string. +The parser transforms a template built from the encoded sides of the parts into a tuple that retains their decoders and checks. Encoding applies the parts' encoders and joins the segments. The parser requires the decoding and encoding services of its parts in the corresponding direction. + +`Schema.toEncoded(parser)` validates that source template. Use `Schema.String` if you need to accept unrestricted strings. + +Ambiguous templates use greedy segmentation with backtracking. Encoding a tuple and decoding the resulting string can produce a different tuple when a segment contains a separator used by the template. + **Example** (Parsing a template literal into components) ```ts @@ -899,25 +1036,12 @@ Failure(Cause([Fail(SchemaError: Custom message */ ``` -### Preserve unexpected keys - -You can preserve unexpected keys by setting `onExcessProperty` to `preserve`. - -**Example** (Preserving unexpected keys) - -```ts -import { Schema } from "effect" - -const schema = Schema.Struct({ - a: Schema.String -}) +### Handling unexpected keys -console.log(String(Schema.decodeUnknownExit(schema)({ a: "a", b: "b" }, { onExcessProperty: "preserve" }))) -/* -Output: -Success({"b":"b","a":"a"}) -*/ -``` +Unexpected keys are ignored by default. Set `onExcessProperty` to `error` to +reject them. To retain additional keys, describe and validate them with +`Schema.Record` or `Schema.StructWithRest` so they are represented in the +schema's type. ### Index Signatures @@ -1683,7 +1807,7 @@ 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** (Keeping the later selected value when parsing sequentially) +**Example** (Keeping the later selected value) ```ts import { Schema, SchemaTransformation } from "effect" @@ -2288,7 +2412,7 @@ const URLSchema = Schema.declare( // The JSON representation is a plain string Schema.String, // How to convert between URL and string - SchemaTransformation.transformOrFail({ + SchemaTransformation.transformEffect({ // JSON string -> URL (may fail if the string is not a valid URL) decode: (s, options) => Effect.try({ @@ -3083,57 +3207,65 @@ Transformation - `RD`: the context used while decoding - `RE`: the context used while encoding -A `Transformation` consists of two `Getter` functions: +A `Transformation` consists of two `Getter` values: - `decode: Getter` — transforms a value during decoding - `encode: Getter` — transforms a value during encoding -Each `Getter` receives an input and an optional context and returns either a value or an error. Getters can be composed to build more complex logic. +Each `Getter` is a tagged description of one operation: + +- `Transform` transforms a present value synchronously. +- `TransformOptional` transforms an `Option` synchronously and can handle a missing value. +- `TransformEffect` and `TransformOptionalEffect` are the corresponding effectful forms. +- `Passthrough` returns its input unchanged. + +Getter values expose `pipe`. Use the dual standalone functions `SchemaGetter.map` and `SchemaGetter.compose` to build +larger transformations. `SchemaGetter.run` executes a getter directly and always returns an `Effect`; schemas execute +their getters through `SchemaParser` instead. **Example** (Implementation of `Transformation.trim`) ```ts +import { SchemaGetter, SchemaTransformation } from "effect" + /** * @category String transformations * @since 4.0.0 */ -export function trim(): Transformation { - return new Transformation(Getter.trim(), Getter.passthrough()) +export function trim(): SchemaTransformation.Transformation { + return new SchemaTransformation.Transformation(SchemaGetter.trim(), SchemaGetter.passthrough()) } ``` In this case: -- The `decode` process uses `Getter.trim()` to remove leading and trailing whitespace. -- The `encode` process uses `Getter.passthrough()`, which returns the input as is. +- The `decode` process uses `SchemaGetter.trim()` to remove leading and trailing whitespace. +- The `encode` process uses `SchemaGetter.passthrough()`, which returns the input as is. ## Composing Transformations -You can combine transformations using the `.compose` method. The resulting transformation applies the `decode` and `encode` logic of both transformations in sequence. +You can combine transformations using `SchemaTransformation.composeTransformation`. The resulting transformation applies the `decode` and `encode` logic of both transformations in sequence. **Example** (Trim and lowercase a string) ```ts -import { Option, SchemaTransformation } from "effect" +import { Schema, SchemaTransformation } from "effect" // Compose two transformations: trim followed by toLowerCase -const trimToLowerCase = SchemaTransformation.trim().compose(SchemaTransformation.toLowerCase()) +const trimToLowerCase = SchemaTransformation.composeTransformation( + SchemaTransformation.trim(), + SchemaTransformation.toLowerCase() +) +const schema = Schema.String.pipe(Schema.decode(trimToLowerCase)) -// Run the decode logic manually to inspect the result -console.log(trimToLowerCase.decode.run(Option.some(" Abc"), {})) -/* -{ - _id: 'Exit', - _tag: 'Success', - value: { _id: 'Option', _tag: 'Some', value: 'abc' } -} -*/ +Schema.decodeUnknownSync(schema)(" Abc") +// "abc" ``` In this example: -- The `decode` logic applies `Getter.trim()` followed by `Getter.toLowerCase()`, producing a string that is trimmed and lowercased. -- The `encode` logic is `Getter.passthrough()`, which simply returns the input as-is. +- The `decode` logic applies `SchemaGetter.trim()` followed by `SchemaGetter.toLowerCase()`, producing a string that is trimmed and lowercased. +- The `encode` logic is `SchemaGetter.passthrough()`, which returns the input unchanged. ## Transforming One Schema into Another @@ -3214,7 +3346,7 @@ const Kilometers = Schema.Finite.pipe( ) ``` -You can define transformations that may fail during decoding or encoding using `SchemaTransformation.transformOrFail`. +You can define transformations that may fail during decoding or encoding using `SchemaTransformation.transformEffect`. This is useful when you need to validate input or enforce rules that may not always succeed. @@ -3226,7 +3358,7 @@ import { Effect, Schema, SchemaIssue, SchemaTransformation } from "effect" const URLFromString = Schema.String.pipe( Schema.decodeTo( Schema.instanceOf(URL), - SchemaTransformation.transformOrFail({ + SchemaTransformation.transformEffect({ decode: (s, options) => Effect.try({ try: () => new URL(s), @@ -5122,6 +5254,29 @@ Schema can derive JSON Schemas, test data generators (Arbitraries), equivalence By default, a schema produces a draft-2020-12 JSON Schema. +The generated document is intended for preliminary validation. JSON Schema and +Effect checks do not always have identical semantics, so the Effect decoder +remains the final authority. Passing JSON Schema validation does not guarantee +that decoding will succeed. + +Properties not modeled by an object schema use `onExcessProperty: "ignore"` by +default, matching the decoder default. This emits `additionalProperties: true`. +Pass `{ onExcessProperty: "error" }` to the generator and decoder to reject +unmatched properties whenever the key space is representable. An +index-signature key check that cannot be represented leaves unmatched +properties open so that JSON Schema does not reject inputs Effect may accept. +The generator does not merge conjunctive key patterns into a new regular +expression. With `onExcessProperty: "ignore"`, it leaves that index signature +open. With `onExcessProperty: "error"`, it uses the generated key schemas under +`propertyNames`. Properties not already selected by `properties` or +`patternProperties` may satisfy any index-signature value schema; the Effect +decoder enforces the exact association between keys and values. + +Known differences include Unicode code-point versus UTF-16 string length, +JavaScript RegExp flags, property checks applied before versus after decoding, +and `oneOf` with overlapping members. Custom `toJsonSchema` callbacks are also +responsible for the semantics they emit. + The result is a data structure including: - the source of the JSON Schema (e.g. `draft-2020-12`, `draft-07`, etc...) @@ -5335,7 +5490,7 @@ console.log(JSON.stringify(document, null, 2)) "type": "string" } }, - "additionalProperties": false + "additionalProperties": true }, "definitions": {} } @@ -5373,7 +5528,7 @@ console.log(JSON.stringify(document, null, 2)) ] } }, - "additionalProperties": false + "additionalProperties": true }, "definitions": {} } @@ -5454,12 +5609,12 @@ console.log(JSON.stringify(document.schema, null, 2)) "required": [ "headers" ], - "additionalProperties": false + "additionalProperties": true } */ // Example (Decode a JSON-safe value using the same serializer) -// If a value matches the JSON Schema above, you can decode it with the serializer. +// JSON Schema is the preliminary check; the serializer remains the final validator. console.log(String(Schema.decodeUnknownExit(serializer)(json))) // Success({"headers":Headers([["a","b"]])}) ``` @@ -5561,7 +5716,7 @@ console.log(JSON.stringify(document, null, 2)) "required": [ "a" ], - "additionalProperties": false + "additionalProperties": true } }, "definitions": {} @@ -6009,7 +6164,7 @@ const json = SchemaRepresentation.toJson( const document = SchemaRepresentation.fromJson(json) const rebuilt = SchemaRepresentation.fromRepresentation(document, { - revivers: [Schema.isMinLengthReviver] + revivers: [SchemaRepresentation.isMinLengthReviver] }) console.log(Schema.is(rebuilt)("abc")) @@ -6018,9 +6173,9 @@ 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. +`SchemaRepresentation` exports individual revivers for built-in declarations and checks, such as +`OptionReviver`, `DateReviver`, and `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. @@ -6033,7 +6188,7 @@ There are separate reviver contracts for opaque declarations, leaf filters, and - `FilterReviver

` - `FilterGroupReviver

` -Use `makeDeclarationReviver`, `makeFilterReviver`, and `makeFilterGroupReviver` to infer `P` from `payloadSchema`. +Use `makeReviverDeclaration`, `makeReviverFilter`, and `makeReviverFilterGroup` to infer `P` from `payloadSchema`. ```ts import { Schema, SchemaRepresentation } from "effect" @@ -6050,7 +6205,7 @@ function minLength( }) } -const minLengthReviver = SchemaRepresentation.makeFilterReviver( +const minLengthReviver = SchemaRepresentation.makeReviverFilter( id, Schema.Struct({ minimum: Schema.Number }), ({ annotations, payload }) => minLength(payload.minimum, annotations) @@ -6070,9 +6225,23 @@ with resolved identifiers and leaves anonymous non-recursive candidates inline, candidates still receive references, using a synthetic name when necessary. Pass `referencePolicy` in the options to use a different allocation rule. +Generated JSON Schema is a preliminary validation layer. The Effect decoder +remains the final authority because string length, RegExp flags, +decoded-object property checks, and `oneOf` can differ between the two +validators. The default `onExcessProperty: "ignore"` emits +`additionalProperties: true`; use `onExcessProperty: "error"` in both +generation and decoding to reject unmatched properties whenever the key space +is representable. Unrepresentable index-signature key checks leave unmatched +properties open under the default mode. The compiler does not merge +conjunctive key patterns into a new regular expression. In `error` mode it uses +the generated key schemas under `propertyNames`. For properties not otherwise +selected, it accepts any index-signature value schema and leaves the exact +key-value association to the Effect decoder. + 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. +Callback authors are responsible for the semantics of their output. `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 @@ -6093,28 +6262,62 @@ schema with revivers first. It does not return a representation document. Only direct local references to top-level definitions in the form `#/$defs/` are supported. Root -references, external references, and pointers below a definition throw an `Unsupported reference` error. A direct -reference to a missing definition throws an `Invalid reference` error. +references, external references, and pointers below a definition are rejected with the supported reference format. +Missing definitions are reported by name. References reached inside a nested schema +resource introduced by `$id` are rejected with their path instead of being resolved against the top-level definitions. +A root `$id`, or a nested `$id` without reached references, is supported. + +Import errors explain which constraint or reference cannot be translated and include its source path. `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. -Import translates a Draft 2020-12 subset. `$dynamicRef`, `contains`, `dependentRequired`, `dependentSchemas`, `not`, -active `if` / `then` / `else`, `unevaluatedItems`, and `unevaluatedProperties` throw an -`Unsupported JSON Schema keyword` error. Inactive conditional keywords and `minContains` / `maxContains` without +Import translates `{ not: {} }` as `Never`, including in optional properties. Other uses of `not` are unsupported. + +Import reuses Effect schemas and built-in checks. It is a best-effort translation, not a guarantee of identical +validation or a lossless round trip: + +- `additionalProperties: true`, `{}`, or an omitted `additionalProperties` imports additional values as `Schema.Json`. + These properties are validated and retained, including with `onExcessProperty: "error"`. +- Closed objects normally have no catch-all for additional properties. The decoder strips excess properties by default; + use `onExcessProperty: "error"` to reject them. Closed empty objects instead import as + `Schema.Record(Schema.String, Schema.Never)` and reject string-keyed entries regardless of that option. + A closed object with a single + `patternProperties` entry and no named or required properties imports as a filtered `Schema.Record` when patterns + are applied. Object keyword scopes still constrain declared properties when intersecting schemas. + Combinations requiring an index signature to exclude + explicit properties or patterned keys remain unsupported and are rejected with an explanation of the limitation. +- With `patterns: "apply"`, open patterned objects are also rejected. Their filtered + index and catch-all would produce incompatible TypeScript index signatures. This includes `additionalProperties` + set to `true`, `{}`, or omitted. An intersection with a closed object can still be imported when it reduces the + result to a finite set of properties. +- `minProperties`, `maxProperties`, and `propertyNames` use the existing checks on the decoded object, after excess + properties have been stripped. No separate validation of the original object is added. +- String length checks count UTF-16 code units, not Unicode code points. For example, `"😀"` satisfies an imported + `minLength: 2` and fails an imported `maxLength: 1`, unlike JSON Schema validation. +- `integer` uses `Schema.isInt` and rejects integers outside JavaScript's safe integer range. +- Applied patterns use the existing `Schema.isPattern` check without adding a Unicode flag. + +An Effect struct exported with `additionalProperties: true` therefore imports with a JSON-valued index signature, +even if the original struct had none. The imported decoder retains additional properties that the original decoder +would strip. Use the imported schema's actual checks and parse options when reasoning about validation. + +Import translates a Draft 2020-12 subset. `$dynamicRef`, `contains`, `dependentRequired`, `dependentSchemas`, +active `if` / `then` / `else`, `unevaluatedItems`, and `unevaluatedProperties` are rejected with an error identifying +the unsupported keyword. Inactive conditional keywords and `minContains` / `maxContains` without `contains` have no validation effect and are ignored. Unknown extension keywords are ignored and their semantics are not -enforced. Objects and arrays used as `const` values or `enum` members throw an -`Unsupported structured JSON Schema value` error. The optional `onEnter` callback can normalize each JSON Schema node +enforced. Objects and arrays used as `const` values or `enum` members are rejected. Only strings, numbers, booleans, and +null are supported. The optional `onEnter` callback can normalize each JSON Schema node before it is translated. Intersections of overlapping unions are limited to disjoint root-type partitions and finite primitive `anyOf` literal -sets. Other union intersections, including cases that would duplicate a nested choice, throw an -`Unsupported intersection of overlapping unions` error. +sets. Other union intersections, including cases that would duplicate a nested choice, are rejected. Regular expression constraints reached during 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 +`patterns: "apply"` only for trusted documents. Set `patterns: "ignore"` to skip reached pattern constraints explicitly. +This can admit values the source rejects, but it can also reject previously valid values inside `oneOf` when removing +constraints makes multiple branches match. 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. @@ -6134,6 +6337,46 @@ Opaque declarations and checks provide code through their `toCode` callbacks. `t 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. +# Parsing Options + +## Concurrent Product Parsing + +The `concurrency` parse option controls how many children of a product schema may parse at the same time. It uses the +same semantics as `Effect.forEach`: `undefined` and `1` are sequential, a number greater than `1` sets a bound, and +`"unbounded"` removes the bound. Synchronous children remain eager and do not require a fiber. + +The option applies to tuple elements, array elements, struct fields, record entries, and structs with rest. It applies +independently at every nested product. For example, with `concurrency: 2`, two outer array elements may each parse two +inner elements concurrently. The option is runtime-only; it is not stored in the schema AST or its representation. + +Union members remain sequential. A product inside the union member currently being evaluated still receives the +option. + +**Example** (Decoding array elements concurrently) + +```ts +import { Effect, Schema, SchemaGetter } from "effect" + +const item = Schema.String.pipe(Schema.decode({ + decode: SchemaGetter.transformEffect((value) => Effect.sleep("10 millis").pipe(Effect.as(value))), + encode: SchemaGetter.passthrough() +})) + +const program = Schema.decodeUnknownEffect(Schema.Array(item))( + ["a", "b", "c"], + { concurrency: 2 } +) + +const result = await Effect.runPromise(program) +// ["a", "b", "c"] +``` + +Tuple and array results retain their element positions, as the result of `Effect.forEach` does. Other observable work +follows completion order. With `errors: "first"`, the first observed child failure terminates the product and interrupts +the remaining children. With `errors: "all"`, issues are accumulated as children complete. If transformed record keys +collide, the last assignment to complete wins. Concurrent work may already have performed effects when another child +fails or the parser interrupts it. + # 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. diff --git a/repos/effect/packages/effect/benchmark/http/serverAllocations.ts b/repos/effect/packages/effect/benchmark/http/serverAllocations.ts new file mode 100644 index 000000000..927a0cac5 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/http/serverAllocations.ts @@ -0,0 +1,138 @@ +import { HttpRouter, HttpServerResponse } from "effect/unstable/http" +import * as inspector from "node:inspector/promises" + +const WARMUP = 5_000 +const REQUESTS = 50_000 +const RETAINED_LIMIT = 64 +const CHURN_LIMIT = 17_000 +const DEFERRED_REQUESTS = 20_000 +const DEFERRED_CONCURRENCY = 32 +// main deferred a per-request span-end task via setImmediate (~4k bytes/request +// held until the loop turns); the native-tracer fast path ends the span inline. +const DEFERRED_LIMIT = 512 + +const gc = (globalThis as any).gc as undefined | (() => void) +if (gc === undefined) { + console.error("run with --expose-gc") + process.exit(1) +} + +const session = new inspector.Session() +session.connect() + +const { dispose, handler } = HttpRouter.toWebHandler( + HttpRouter.add("GET", "/ping", HttpServerResponse.text("pong")), + { disableLogger: true } +) + +const run = async (count: number) => { + for (let i = 0; i < count; i++) { + const response = await handler(new Request("http://localhost/ping")) + await response.arrayBuffer() + } +} + +// a promise-chained load never yields to the event loop, so any per-request +// macrotask (setImmediate) the request path schedules accumulates unrun +const runConcurrent = async (count: number) => { + let remaining = count + const worker = async () => { + while (remaining > 0) { + remaining-- + const response = await handler(new Request("http://localhost/ping")) + await response.arrayBuffer() + } + } + await Promise.all(Array.from({ length: DEFERRED_CONCURRENCY }, worker)) +} + +interface ProfileNode { + callFrame: { functionName: string; url: string; lineNumber: number } + selfSize: number + children?: Array +} + +const topFrames = (head: ProfileNode) => { + const byFrame = new Map() + let total = 0 + const visit = (node: ProfileNode) => { + if (node.selfSize > 0) { + const f = node.callFrame + const url = f.url.replace(/^.*\/(packages|node_modules)\//, "$1/") + const key = `${f.functionName || "(anonymous)"} @ ${url}:${f.lineNumber + 1}` + byFrame.set(key, (byFrame.get(key) ?? 0) + node.selfSize) + total += node.selfSize + } + node.children?.forEach(visit) + } + visit(head) + return { + total, + frames: [...byFrame.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15) + } +} + +// Allow FinalizationRegistry callbacks to run before measuring retained heap. +const heapUsed = async () => { + for (let i = 0; i < 5; i++) { + gc() + await new Promise((resolve) => setImmediate(resolve)) + } + return process.memoryUsage().heapUsed +} + +await run(WARMUP) + +const heapBefore = await heapUsed() +await run(REQUESTS) +const heapAfter = await heapUsed() + +// A promise-chained burst never yields to the event loop, so synchronous-GC +// heap deltas expose per-request work deferred into macrotask queues. +await runConcurrent(WARMUP) +gc() +gc() +const deferredBefore = process.memoryUsage().heapUsed +await runConcurrent(DEFERRED_REQUESTS) +gc() +gc() +const deferredAfter = process.memoryUsage().heapUsed + +await session.post("HeapProfiler.startSampling", { + samplingInterval: 16384, + includeObjectsCollectedByMajorGC: true, + includeObjectsCollectedByMinorGC: true +}) +await run(REQUESTS) +const { profile } = await session.post("HeapProfiler.stopSampling") + +await dispose() + +const { frames, total } = topFrames(profile.head as ProfileNode) +const churnPerRequest = Math.round(total / REQUESTS) +const retainedPerRequest = Math.round((heapAfter - heapBefore) / REQUESTS) +const deferredPerRequest = Math.round((deferredAfter - deferredBefore) / DEFERRED_REQUESTS) + +console.log(`requests: ${REQUESTS}`) +console.log(`allocated/request: ${churnPerRequest} bytes (limit ${CHURN_LIMIT})`) +console.log(`retained/request: ${retainedPerRequest} bytes (limit ${RETAINED_LIMIT})`) +console.log(`deferred/request: ${deferredPerRequest} bytes (limit ${DEFERRED_LIMIT})`) +console.log("top allocation sites:") +for (const [key, size] of frames) { + const pct = ((size / total) * 100).toFixed(1).padStart(5) + console.log(` ${pct}% ${(size / 1024 / 1024).toFixed(2).padStart(7)} MiB ${key}`) +} + +if (retainedPerRequest > RETAINED_LIMIT) { + console.error(`FAIL: retained heap grows ${retainedPerRequest} bytes/request`) + process.exit(1) +} +if (deferredPerRequest > DEFERRED_LIMIT) { + console.error(`FAIL: microtask-only burst holds ${deferredPerRequest} bytes/request exceeds ${DEFERRED_LIMIT}`) + process.exit(1) +} +if (churnPerRequest > CHURN_LIMIT) { + console.error(`FAIL: allocation churn ${churnPerRequest} bytes/request exceeds ${CHURN_LIMIT}`) + process.exit(1) +} +console.log("PASS") diff --git a/repos/effect/packages/effect/package.json b/repos/effect/packages/effect/package.json index 15d5aae5a..abc2681bf 100644 --- a/repos/effect/packages/effect/package.json +++ b/repos/effect/packages/effect/package.json @@ -1,7 +1,7 @@ { "name": "effect", "type": "module", - "version": "4.0.0-rc.112", + "version": "4.0.0-rc.117", "license": "MIT", "description": "The missing standard library for TypeScript, for writing production-grade software.", "homepage": "https://effect.website", @@ -25,7 +25,10 @@ "concurrency", "observability" ], - "sideEffects": [], + "sideEffects": [ + "./src/unstable/schema/SchemaJITCompiler/enable.ts", + "./dist/unstable/schema/SchemaJITCompiler/enable.js" + ], "exports": { "./package.json": "./package.json", ".": "./src/index.ts", @@ -39,6 +42,7 @@ "./unstable/eventlog": "./src/unstable/eventlog/index.ts", "./unstable/http": "./src/unstable/http/index.ts", "./unstable/httpapi": "./src/unstable/httpapi/index.ts", + "./unstable/net": "./src/unstable/net/index.ts", "./unstable/observability": "./src/unstable/observability/index.ts", "./unstable/persistence": "./src/unstable/persistence/index.ts", "./unstable/process": "./src/unstable/process/index.ts", @@ -82,6 +86,7 @@ "./unstable/eventlog": "./dist/unstable/eventlog/index.js", "./unstable/http": "./dist/unstable/http/index.js", "./unstable/httpapi": "./dist/unstable/httpapi/index.js", + "./unstable/net": "./dist/unstable/net/index.js", "./unstable/observability": "./dist/unstable/observability/index.js", "./unstable/persistence": "./dist/unstable/persistence/index.js", "./unstable/process": "./dist/unstable/process/index.js", @@ -107,13 +112,13 @@ "check": "tsc -b tsconfig.json" }, "devDependencies": { - "@types/node": "^26.4.0", + "@types/node": "^26.6.2", "ajv": "^8.20.0", "ajv-draft-04": "^1.0.0", - "ast-types": "^0.16.1", + "ast-types": "^0.16.3", "immer": "^11.1.18", "protobufjs": "^8.8.0", - "tinybench": "^6.1.4", - "valibot": "^1.4.2" + "tinybench": "^6.2.0", + "valibot": "^1.5.0" } } diff --git a/repos/effect/packages/effect/runtimeperf/README.md b/repos/effect/packages/effect/runtimeperf/README.md index 8265911a7..bbfc32a6e 100644 --- a/repos/effect/packages/effect/runtimeperf/README.md +++ b/repos/effect/packages/effect/runtimeperf/README.md @@ -42,6 +42,7 @@ pnpm runtimeperf object-32-valid pnpm runtimeperf schema/object-32-valid-effect pnpm runtimeperf --family arrays pnpm runtimeperf --implementation zod4 +pnpm runtimeperf --implementation zod4-compiled ``` Override measurement settings: @@ -86,6 +87,13 @@ adapters, recursion and cold paths. The `schema-benchmarks` suite contains the complete timing matrices exposed by the upstream Effect, Valibot and Zod adapters. +The `moltar-parse-safe` and `moltar-assert-loose` suites preserve Moltar's +object shape. Their valid cases reproduce the upstream timed operation; the +extra-property and invalid cases are Effect extensions. They compare +interpreted Effect, Effect JIT and AOT, Valibot, ordinary and jitless Zod where +applicable, and Zod `compile`, using the dependency versions recorded in each +report. + The `arbitrary` suite compares the native public API with direct fast-check v4 arbitraries in separate processes. It measures derivation through the first recursive sample, steady-state recursive sampling, optional-Struct sampling, fixed-length string generation to exercise constraint pushdown, @@ -103,6 +111,11 @@ end-to-end public API comparison, but the work is not identical: native replay verifies the original failure and its full shrink path, while fast-check can start directly from its recorded path. +Array cases cover passing checks and shrinking a four-command sequence while preserving an ordered pair. The latter +starts from the same explicit sequence in both engines, using a fixed seed for native generation and an example for +fast-check. Base/head validation preserves the failure without requiring identical shrunk outputs, so its timing also +reflects changes in how much of the shrink tree is explored. The runtime regression tests assert the smaller result. + The regular-expression fixtures validate the same language, and both fixed-seed warm fixtures must reach multiple lengths and every alternative family. Their exact distributions remain implementation-defined, so cross-library timings are diagnostic while base/head comparisons protect the cost of each implementation's established behavior. @@ -118,13 +131,18 @@ 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. +The compiler comparison calls `z.compile(schema, { strict: true })` and uses +Zod's `validate` API for boolean checks. Ten representative scenarios also run +against equivalent Valibot schemas using `parse` and `is`. ## 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. +Each worker validates the fixture before and after measuring. The Effect +fixture calibrates one batch size that every implementation in the same +scenario uses. This keeps the enclosing loop identical across implementations; +V8 can otherwise optimize sub-10 ns callbacks differently at different batch +sizes. Each implementation executes in a separate process, with rotating order +within the scenario. Tinybench measures one synchronous batched task. The primary process result is: diff --git a/repos/effect/packages/effect/runtimeperf/compare.mts b/repos/effect/packages/effect/runtimeperf/compare.mts index 7c2cf346e..1b08f7c4e 100644 --- a/repos/effect/packages/effect/runtimeperf/compare.mts +++ b/repos/effect/packages/effect/runtimeperf/compare.mts @@ -1,13 +1,14 @@ import { spawnSync } from "node:child_process" -import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs" +import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs" import os from "node:os" -import { join } from "node:path" +import { dirname, join } from "node:path" import process from "node:process" import { materializeFixture } from "./materialize.mts" import { analyzePairs } from "./stats.mts" import { aggregateMeasurements, calibrateFixture, + comparePath, configPath, coverageSummary, effectDir, @@ -16,6 +17,7 @@ import { libraryVersions, loadRegistry, makeRunId, + materializePath, measureFixture, parseArgs, printTable, @@ -25,6 +27,8 @@ import { resolveDefaults, selectFixtures, sha256, + statsPath, + utilsPath, workerPath, writeJson } from "./utils.mts" @@ -90,6 +94,22 @@ const createWorktree = (runRoot, name, sha) => { return path } +const applyWorktreeChanges = (path, untracked) => { + const diff = runGit(["diff", "--binary", "HEAD", "--"]) + if (diff !== "") { + const result = run("git", ["apply", "--binary", "-"], { cwd: path, input: `${diff}\n` }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${result.stdout}${result.stderr}`.trim()) + } + } + for (const file of untracked) { + const target = join(path, file.path) + mkdirSync(dirname(target), { recursive: true }) + cpSync(join(repoRoot, file.path), target, { recursive: true }) + } +} + const worktreeState = () => { const diff = runGit(["diff", "--binary", "HEAD", "--"]) const untrackedOutput = runGit([ @@ -136,16 +156,13 @@ const main = () => { 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) + const headRoot = createWorktree(runRoot, "head", headSha) + worktrees.push(headRoot) + if (options.head === "worktree") applyWorktreeChanges(headRoot, state.untracked) for (const fixture of selected) { const baseFixturePath = materializeFixture(baseRoot, fixture) - const headFixturePath = headRoot === repoRoot - ? fixture.fixturePath - : materializeFixture(headRoot, fixture) + const headFixturePath = materializeFixture(headRoot, fixture) const baseCalibration = calibrateFixture(fixture, defaults, baseFixturePath) const headCalibration = calibrateFixture(fixture, defaults, headFixturePath) const batchSize = Math.max(baseCalibration.batchSize, headCalibration.batchSize) @@ -227,7 +244,11 @@ const main = () => { artifactMode: "repository", coverage: coverageSummary(selected), hashes: { + compare: hashFile(comparePath), config: hashFile(configPath), + materialize: hashFile(materializePath), + stats: hashFile(statsPath), + utils: hashFile(utilsPath), worker: hashFile(workerPath), fixtures: Object.fromEntries( [...new Set(selected.map((fixture) => fixture.fixturePath))] diff --git a/repos/effect/packages/effect/runtimeperf/config.json b/repos/effect/packages/effect/runtimeperf/config.json index 261711dcd..4258f6083 100644 --- a/repos/effect/packages/effect/runtimeperf/config.json +++ b/repos/effect/packages/effect/runtimeperf/config.json @@ -322,6 +322,21 @@ ], "size": 1 }, + { + "name": "array-check-pass-100-native", + "export": "arrayCheckPass100", + "scenario": "arbitrary-array-check-pass-100", + "operation": "check-array", + "size": 50 + }, + { + "name": "array-check-falsify-shrink-native", + "export": "arrayCheckFalsifyAndShrink", + "scenario": "arbitrary-array-check-falsify-shrink", + "operation": "check-array-and-shrink", + "path": "failure", + "size": 4 + }, { "name": "check-pass-100-native", "export": "checkPass100", @@ -571,6 +586,21 @@ "path": "failure", "size": 1 }, + { + "name": "array-check-pass-100-fast-check-v4", + "export": "arrayCheckPass100", + "scenario": "arbitrary-array-check-pass-100", + "operation": "check-array", + "size": 50 + }, + { + "name": "array-check-falsify-shrink-fast-check-v4", + "export": "arrayCheckFalsifyAndShrink", + "scenario": "arbitrary-array-check-falsify-shrink", + "operation": "check-array-and-shrink", + "path": "failure", + "size": 4 + }, { "name": "check-pass-100-fast-check-v4", "export": "checkPass100", @@ -1042,6 +1072,45 @@ "path": "valid", "size": 32 }, + { + "name": "array-32-suspended-concurrency-4", + "export": "array32SuspendedConcurrent4", + "scenario": "array-32-suspended-concurrency-4", + "family": "arrays", + "astTags": [ + "Arrays", + "String", + "Transformation" + ], + "path": "valid", + "size": 32 + }, + { + "name": "object-32-suspended-concurrency-4", + "export": "object32SuspendedConcurrent4", + "scenario": "object-32-suspended-concurrency-4", + "family": "objects", + "astTags": [ + "Objects", + "String", + "Transformation" + ], + "path": "valid", + "size": 32 + }, + { + "name": "record-32-suspended-concurrency-4", + "export": "record32SuspendedConcurrent4", + "scenario": "record-32-suspended-concurrency-4", + "family": "records", + "astTags": [ + "Objects", + "String", + "Transformation" + ], + "path": "valid", + "size": 32 + }, { "name": "literal-2-valid-last", "export": "literal2ValidLast", @@ -1104,17 +1173,6 @@ "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", @@ -1143,6 +1201,13 @@ "size": 2 }, "cases": [ + { + "name": "adapter-make-valid", + "export": "makeValid", + "scenario": "adapter-make-valid", + "operation": "make", + "adapter": "Sync" + }, { "name": "adapter-parser-decode-exit-invalid", "export": "parserExitInvalid", @@ -1218,6 +1283,44 @@ "size": 32 }, "cases": [ + { + "name": "schema-creation-object-2-effect", + "export": "effectSchemaCreationObject2", + "scenario": "schema-creation-object-2", + "implementation": "effect", + "operation": "schema", + "size": 2 + }, + { + "name": "schema-creation-object-32-effect", + "export": "effectSchemaCreationObject32", + "scenario": "schema-creation-object-32", + "implementation": "effect", + "operation": "schema" + }, + { + "name": "schema-creation-object-256-effect", + "export": "effectSchemaCreationObject256", + "scenario": "schema-creation-object-256", + "implementation": "effect", + "operation": "schema", + "size": 256 + }, + { + "name": "schema-creation-encoded-record-effect", + "export": "effectSchemaCreationEncodedRecord", + "scenario": "schema-creation-encoded-record", + "implementation": "effect", + "operation": "schema" + }, + { + "name": "first-make-object-2-effect", + "export": "effectFirstMakeObject2", + "scenario": "first-make-object-2", + "implementation": "effect", + "operation": "first-make", + "size": 2 + }, { "name": "schema-creation-template-literal-effect", "export": "effectSchemaCreationTemplateLiteral", @@ -1229,6 +1332,17 @@ ], "size": 5 }, + { + "name": "schema-creation-template-literal-256-effect", + "export": "effectSchemaCreationTemplateLiteral256", + "scenario": "schema-creation-template-literal-256", + "implementation": "effect", + "operation": "schema", + "astTags": [ + "TemplateLiteral" + ], + "size": 256 + }, { "name": "first-decode-checked-object-32-effect", "export": "effectFirstDecodeCheckedObject32", @@ -1589,6 +1703,1536 @@ ] } ] + }, + { + "name": "moltar-parse-safe", + "fixtures": [ + { + "file": "suites/moltar/fixtures/interpreted.ts", + "defaults": { + "tier": 2, + "family": "moltar-parse-safe", + "implementation": "effect", + "operation": "decode-unknown-sync", + "astTags": ["Objects", "String", "Number", "Boolean"], + "size": "moltar-object" + }, + "cases": [ + { + "name": "effect-valid", + "export": "parseValid", + "scenario": "moltar-parse-safe-valid", + "path": "valid" + }, + { + "name": "effect-extra-valid", + "export": "parseExtraValid", + "scenario": "moltar-parse-safe-extra-valid", + "path": "extra-valid" + }, + { + "name": "effect-invalid", + "export": "parseInvalid", + "scenario": "moltar-parse-safe-invalid", + "path": "invalid" + } + ] + }, + { + "file": "suites/moltar/fixtures/jit.ts", + "defaults": { + "tier": 2, + "family": "moltar-parse-safe", + "implementation": "effect-jit", + "operation": "decode-unknown-sync", + "astTags": ["Objects", "String", "Number", "Boolean"], + "size": "moltar-object" + }, + "cases": [ + { + "name": "effect-jit-valid", + "export": "parseValid", + "scenario": "moltar-parse-safe-valid", + "path": "valid" + }, + { + "name": "effect-jit-extra-valid", + "export": "parseExtraValid", + "scenario": "moltar-parse-safe-extra-valid", + "path": "extra-valid" + }, + { + "name": "effect-jit-invalid", + "export": "parseInvalid", + "scenario": "moltar-parse-safe-invalid", + "path": "invalid" + } + ] + }, + { + "file": "suites/moltar/fixtures/aot.ts", + "defaults": { + "tier": 2, + "family": "moltar-parse-safe", + "implementation": "effect-aot", + "operation": "decode-unknown-sync", + "astTags": ["Objects", "String", "Number", "Boolean"], + "size": "moltar-object" + }, + "cases": [ + { + "name": "effect-aot-valid", + "export": "parseValid", + "scenario": "moltar-parse-safe-valid", + "path": "valid" + }, + { + "name": "effect-aot-extra-valid", + "export": "parseExtraValid", + "scenario": "moltar-parse-safe-extra-valid", + "path": "extra-valid" + }, + { + "name": "effect-aot-invalid", + "export": "parseInvalid", + "scenario": "moltar-parse-safe-invalid", + "path": "invalid" + } + ] + }, + { + "file": "suites/moltar/fixtures/valibot.ts", + "defaults": { + "tier": 2, + "family": "moltar-parse-safe", + "implementation": "valibot", + "operation": "parse", + "astTags": [], + "size": "moltar-object" + }, + "cases": [ + { + "name": "valibot-valid", + "export": "parseValid", + "scenario": "moltar-parse-safe-valid", + "path": "valid" + }, + { + "name": "valibot-extra-valid", + "export": "parseExtraValid", + "scenario": "moltar-parse-safe-extra-valid", + "path": "extra-valid" + }, + { + "name": "valibot-invalid", + "export": "parseInvalid", + "scenario": "moltar-parse-safe-invalid", + "path": "invalid" + } + ] + }, + { + "file": "suites/moltar/fixtures/zod.ts", + "defaults": { + "tier": 2, + "family": "moltar-parse-safe", + "astTags": [], + "size": "moltar-object" + }, + "cases": [ + { + "name": "zod-valid", + "export": "parseValid", + "scenario": "moltar-parse-safe-valid", + "implementation": "zod4", + "operation": "parse", + "path": "valid" + }, + { + "name": "zod-extra-valid", + "export": "parseExtraValid", + "scenario": "moltar-parse-safe-extra-valid", + "implementation": "zod4", + "operation": "parse", + "path": "extra-valid" + }, + { + "name": "zod-invalid", + "export": "parseInvalid", + "scenario": "moltar-parse-safe-invalid", + "implementation": "zod4", + "operation": "parse", + "path": "invalid" + }, + { + "name": "zod-jitless-valid", + "export": "parseJitlessValid", + "scenario": "moltar-parse-safe-valid", + "implementation": "zod4-jitless", + "operation": "parse-jitless", + "path": "valid" + }, + { + "name": "zod-jitless-extra-valid", + "export": "parseJitlessExtraValid", + "scenario": "moltar-parse-safe-extra-valid", + "implementation": "zod4-jitless", + "operation": "parse-jitless", + "path": "extra-valid" + }, + { + "name": "zod-jitless-invalid", + "export": "parseJitlessInvalid", + "scenario": "moltar-parse-safe-invalid", + "implementation": "zod4-jitless", + "operation": "parse-jitless", + "path": "invalid" + }, + { + "name": "zod-compiled-valid", + "export": "parseCompiledValid", + "scenario": "moltar-parse-safe-valid", + "implementation": "zod4-compiled", + "operation": "parse-compiled", + "path": "valid" + }, + { + "name": "zod-compiled-extra-valid", + "export": "parseCompiledExtraValid", + "scenario": "moltar-parse-safe-extra-valid", + "implementation": "zod4-compiled", + "operation": "parse-compiled", + "path": "extra-valid" + }, + { + "name": "zod-compiled-invalid", + "export": "parseCompiledInvalid", + "scenario": "moltar-parse-safe-invalid", + "implementation": "zod4-compiled", + "operation": "parse-compiled", + "path": "invalid" + } + ] + } + ] + }, + { + "name": "moltar-assert-loose", + "fixtures": [ + { + "file": "suites/moltar/fixtures/interpreted.ts", + "defaults": { + "tier": 2, + "family": "moltar-assert-loose", + "implementation": "effect", + "operation": "is", + "astTags": ["Objects", "String", "Number", "Boolean"], + "size": "moltar-object" + }, + "cases": [ + { + "name": "effect-valid", + "export": "isValid", + "scenario": "moltar-assert-loose-valid", + "path": "valid" + }, + { + "name": "effect-extra-valid", + "export": "isExtraValid", + "scenario": "moltar-assert-loose-extra-valid", + "path": "extra-valid" + }, + { + "name": "effect-invalid", + "export": "isInvalid", + "scenario": "moltar-assert-loose-invalid", + "path": "invalid" + } + ] + }, + { + "file": "suites/moltar/fixtures/jit.ts", + "defaults": { + "tier": 2, + "family": "moltar-assert-loose", + "implementation": "effect-jit", + "operation": "is", + "astTags": ["Objects", "String", "Number", "Boolean"], + "size": "moltar-object" + }, + "cases": [ + { + "name": "effect-jit-valid", + "export": "isValid", + "scenario": "moltar-assert-loose-valid", + "path": "valid" + }, + { + "name": "effect-jit-extra-valid", + "export": "isExtraValid", + "scenario": "moltar-assert-loose-extra-valid", + "path": "extra-valid" + }, + { + "name": "effect-jit-invalid", + "export": "isInvalid", + "scenario": "moltar-assert-loose-invalid", + "path": "invalid" + } + ] + }, + { + "file": "suites/moltar/fixtures/aot.ts", + "defaults": { + "tier": 2, + "family": "moltar-assert-loose", + "implementation": "effect-aot", + "operation": "is", + "astTags": ["Objects", "String", "Number", "Boolean"], + "size": "moltar-object" + }, + "cases": [ + { + "name": "effect-aot-valid", + "export": "isValid", + "scenario": "moltar-assert-loose-valid", + "path": "valid" + }, + { + "name": "effect-aot-extra-valid", + "export": "isExtraValid", + "scenario": "moltar-assert-loose-extra-valid", + "path": "extra-valid" + }, + { + "name": "effect-aot-invalid", + "export": "isInvalid", + "scenario": "moltar-assert-loose-invalid", + "path": "invalid" + } + ] + }, + { + "file": "suites/moltar/fixtures/valibot.ts", + "defaults": { + "tier": 2, + "family": "moltar-assert-loose", + "implementation": "valibot", + "operation": "is", + "astTags": [], + "size": "moltar-object" + }, + "cases": [ + { + "name": "valibot-valid", + "export": "isValid", + "scenario": "moltar-assert-loose-valid", + "path": "valid" + }, + { + "name": "valibot-extra-valid", + "export": "isExtraValid", + "scenario": "moltar-assert-loose-extra-valid", + "path": "extra-valid" + }, + { + "name": "valibot-invalid", + "export": "isInvalid", + "scenario": "moltar-assert-loose-invalid", + "path": "invalid" + } + ] + }, + { + "file": "suites/moltar/fixtures/zod.ts", + "defaults": { + "tier": 2, + "family": "moltar-assert-loose", + "astTags": [], + "size": "moltar-object" + }, + "cases": [ + { + "name": "zod-parse-valid", + "export": "assertParseValid", + "scenario": "moltar-assert-loose-valid", + "implementation": "zod4", + "operation": "parse-and-assert", + "path": "valid" + }, + { + "name": "zod-parse-extra-valid", + "export": "assertParseExtraValid", + "scenario": "moltar-assert-loose-extra-valid", + "implementation": "zod4", + "operation": "parse-and-assert", + "path": "extra-valid" + }, + { + "name": "zod-jitless-validate-valid", + "export": "isJitlessValid", + "scenario": "moltar-assert-loose-valid", + "implementation": "zod4-jitless", + "operation": "validate-jitless", + "path": "valid" + }, + { + "name": "zod-jitless-validate-extra-valid", + "export": "isJitlessExtraValid", + "scenario": "moltar-assert-loose-extra-valid", + "implementation": "zod4-jitless", + "operation": "validate-jitless", + "path": "extra-valid" + }, + { + "name": "zod-jitless-validate-invalid", + "export": "isJitlessInvalid", + "scenario": "moltar-assert-loose-invalid", + "implementation": "zod4-jitless", + "operation": "validate-jitless", + "path": "invalid" + }, + { + "name": "zod-validate-valid", + "export": "isValid", + "scenario": "moltar-assert-loose-valid", + "implementation": "zod4-validate", + "operation": "validate", + "path": "valid" + }, + { + "name": "zod-validate-extra-valid", + "export": "isExtraValid", + "scenario": "moltar-assert-loose-extra-valid", + "implementation": "zod4-validate", + "operation": "validate", + "path": "extra-valid" + }, + { + "name": "zod-validate-invalid", + "export": "isInvalid", + "scenario": "moltar-assert-loose-invalid", + "implementation": "zod4-validate", + "operation": "validate", + "path": "invalid" + }, + { + "name": "zod-compiled-valid", + "export": "isCompiledValid", + "scenario": "moltar-assert-loose-valid", + "implementation": "zod4-compiled", + "operation": "validate-compiled", + "path": "valid" + }, + { + "name": "zod-compiled-extra-valid", + "export": "isCompiledExtraValid", + "scenario": "moltar-assert-loose-extra-valid", + "implementation": "zod4-compiled", + "operation": "validate-compiled", + "path": "extra-valid" + }, + { + "name": "zod-compiled-invalid", + "export": "isCompiledInvalid", + "scenario": "moltar-assert-loose-invalid", + "implementation": "zod4-compiled", + "operation": "validate-compiled", + "path": "invalid" + } + ] + } + ] + }, + { + "name": "compiler-rebuild", + "fixtures": [ + { + "file": "suites/compiler-rebuild/fixtures/interpreted.ts", + "defaults": { + "tier": 1, + "family": "interpreted", + "implementation": "effect", + "astTags": [ + "Objects", + "String", + "Number", + "Boolean" + ], + "operation": "decode", + "path": "valid", + "size": 3 + }, + "cases": [ + { + "name": "interpreted-parseValid", + "export": "parseValid", + "scenario": "compiler-rebuild-parseValid" + }, + { + "name": "interpreted-parseExtra", + "export": "parseExtra", + "scenario": "compiler-rebuild-parseExtra", + "size": 4 + }, + { + "name": "interpreted-parseInvalid", + "export": "parseInvalid", + "scenario": "compiler-rebuild-parseInvalid", + "path": "invalid" + }, + { + "name": "interpreted-isValid", + "export": "isValid", + "scenario": "compiler-rebuild-isValid", + "operation": "is" + }, + { + "name": "interpreted-isInvalid", + "export": "isInvalid", + "scenario": "compiler-rebuild-isInvalid", + "operation": "is", + "path": "invalid" + }, + { + "name": "interpreted-encode", + "export": "encode", + "scenario": "compiler-rebuild-encode", + "operation": "encode" + }, + { + "name": "interpreted-strict", + "export": "strict", + "scenario": "compiler-rebuild-strict" + }, + { + "name": "interpreted-nested", + "export": "nested", + "scenario": "compiler-rebuild-nested", + "astTags": [ + "Objects", + "Arrays", + "String", + "Number", + "Boolean" + ], + "size": 2 + }, + { + "name": "interpreted-array", + "export": "array", + "scenario": "compiler-rebuild-array", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "interpreted-arrayInvalid", + "export": "arrayInvalid", + "scenario": "compiler-rebuild-arrayInvalid", + "path": "invalid", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 33 + }, + { + "name": "interpreted-tuple", + "export": "tuple", + "scenario": "compiler-rebuild-tuple", + "astTags": [ + "Arrays", + "String", + "Number", + "Boolean" + ], + "size": 34 + }, + { + "name": "interpreted-record", + "export": "record", + "scenario": "compiler-rebuild-record", + "size": 32 + }, + { + "name": "interpreted-recordTransformedKeys", + "export": "recordTransformedKeys", + "scenario": "compiler-rebuild-recordTransformedKeys", + "astTags": [ + "Objects", + "String" + ], + "size": 32 + }, + { + "name": "interpreted-union", + "export": "union", + "scenario": "compiler-rebuild-union", + "astTags": [ + "Union", + "Objects", + "Literal", + "Number" + ], + "size": 8 + }, + { + "name": "interpreted-oneOf", + "export": "oneOf", + "scenario": "compiler-rebuild-oneOf", + "astTags": [ + "Union", + "Objects", + "String", + "Number" + ], + "size": 2 + }, + { + "name": "interpreted-transform", + "export": "transform", + "scenario": "compiler-rebuild-transform", + "astTags": [ + "Objects", + "String", + "Number" + ], + "size": 32 + }, + { + "name": "interpreted-transformInvalid", + "export": "transformInvalid", + "scenario": "compiler-rebuild-transformInvalid", + "path": "invalid", + "astTags": [ + "String", + "Number" + ], + "size": 1 + }, + { + "name": "interpreted-rootTransform", + "export": "rootTransform", + "scenario": "compiler-rebuild-rootTransform", + "astTags": [ + "String", + "Number" + ], + "size": 1 + }, + { + "name": "interpreted-middleware", + "export": "middleware", + "scenario": "compiler-rebuild-middleware" + }, + { + "name": "interpreted-recursive", + "export": "recursive", + "scenario": "compiler-rebuild-recursive", + "astTags": [ + "Objects", + "Arrays", + "Suspend", + "Number" + ], + "size": 31 + }, + { + "name": "interpreted-suspendRoot", + "export": "suspendRoot", + "scenario": "compiler-rebuild-suspendRoot", + "astTags": [ + "Suspend", + "Objects", + "Number" + ], + "size": 1 + }, + { + "name": "interpreted-declaration", + "export": "declaration", + "scenario": "compiler-rebuild-declaration", + "astTags": [ + "Declaration", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "interpreted-makeStruct", + "export": "makeStruct", + "scenario": "compiler-rebuild-makeStruct", + "operation": "make", + "astTags": [ + "Objects", + "Number" + ], + "size": 32 + }, + { + "name": "interpreted-makeArray", + "export": "makeArray", + "scenario": "compiler-rebuild-makeArray", + "operation": "make", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "interpreted-makeUnion", + "export": "makeUnion", + "scenario": "compiler-rebuild-makeUnion", + "operation": "make", + "astTags": [ + "Union", + "Objects", + "Literal", + "Number" + ], + "size": 8 + } + ] + }, + { + "file": "suites/compiler-rebuild/fixtures/jit.ts", + "defaults": { + "tier": 1, + "family": "jit", + "implementation": "effect-jit", + "astTags": [ + "Objects", + "String", + "Number", + "Boolean" + ], + "operation": "decode", + "path": "valid", + "size": 3 + }, + "cases": [ + { + "name": "jit-parseValid", + "export": "parseValid", + "scenario": "compiler-rebuild-parseValid" + }, + { + "name": "jit-parseExtra", + "export": "parseExtra", + "scenario": "compiler-rebuild-parseExtra", + "size": 4 + }, + { + "name": "jit-parseInvalid", + "export": "parseInvalid", + "scenario": "compiler-rebuild-parseInvalid", + "path": "invalid" + }, + { + "name": "jit-isValid", + "export": "isValid", + "scenario": "compiler-rebuild-isValid", + "operation": "is" + }, + { + "name": "jit-isInvalid", + "export": "isInvalid", + "scenario": "compiler-rebuild-isInvalid", + "operation": "is", + "path": "invalid" + }, + { + "name": "jit-encode", + "export": "encode", + "scenario": "compiler-rebuild-encode", + "operation": "encode" + }, + { + "name": "jit-strict", + "export": "strict", + "scenario": "compiler-rebuild-strict" + }, + { + "name": "jit-nested", + "export": "nested", + "scenario": "compiler-rebuild-nested", + "astTags": [ + "Objects", + "Arrays", + "String", + "Number", + "Boolean" + ], + "size": 2 + }, + { + "name": "jit-array", + "export": "array", + "scenario": "compiler-rebuild-array", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "jit-arrayInvalid", + "export": "arrayInvalid", + "scenario": "compiler-rebuild-arrayInvalid", + "path": "invalid", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 33 + }, + { + "name": "jit-tuple", + "export": "tuple", + "scenario": "compiler-rebuild-tuple", + "astTags": [ + "Arrays", + "String", + "Number", + "Boolean" + ], + "size": 34 + }, + { + "name": "jit-record", + "export": "record", + "scenario": "compiler-rebuild-record", + "size": 32 + }, + { + "name": "jit-recordTransformedKeys", + "export": "recordTransformedKeys", + "scenario": "compiler-rebuild-recordTransformedKeys", + "astTags": [ + "Objects", + "String" + ], + "size": 32 + }, + { + "name": "jit-union", + "export": "union", + "scenario": "compiler-rebuild-union", + "astTags": [ + "Union", + "Objects", + "Literal", + "Number" + ], + "size": 8 + }, + { + "name": "jit-oneOf", + "export": "oneOf", + "scenario": "compiler-rebuild-oneOf", + "astTags": [ + "Union", + "Objects", + "String", + "Number" + ], + "size": 2 + }, + { + "name": "jit-transform", + "export": "transform", + "scenario": "compiler-rebuild-transform", + "astTags": [ + "Objects", + "String", + "Number" + ], + "size": 32 + }, + { + "name": "jit-transformInvalid", + "export": "transformInvalid", + "scenario": "compiler-rebuild-transformInvalid", + "path": "invalid", + "astTags": [ + "String", + "Number" + ], + "size": 1 + }, + { + "name": "jit-rootTransform", + "export": "rootTransform", + "scenario": "compiler-rebuild-rootTransform", + "astTags": [ + "String", + "Number" + ], + "size": 1 + }, + { + "name": "jit-middleware", + "export": "middleware", + "scenario": "compiler-rebuild-middleware" + }, + { + "name": "jit-recursive", + "export": "recursive", + "scenario": "compiler-rebuild-recursive", + "astTags": [ + "Objects", + "Arrays", + "Suspend", + "Number" + ], + "size": 31 + }, + { + "name": "jit-suspendRoot", + "export": "suspendRoot", + "scenario": "compiler-rebuild-suspendRoot", + "astTags": [ + "Suspend", + "Objects", + "Number" + ], + "size": 1 + }, + { + "name": "jit-declaration", + "export": "declaration", + "scenario": "compiler-rebuild-declaration", + "astTags": [ + "Declaration", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "jit-makeStruct", + "export": "makeStruct", + "scenario": "compiler-rebuild-makeStruct", + "operation": "make", + "astTags": [ + "Objects", + "Number" + ], + "size": 32 + }, + { + "name": "jit-makeArray", + "export": "makeArray", + "scenario": "compiler-rebuild-makeArray", + "operation": "make", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "jit-makeUnion", + "export": "makeUnion", + "scenario": "compiler-rebuild-makeUnion", + "operation": "make", + "astTags": [ + "Union", + "Objects", + "Literal", + "Number" + ], + "size": 8 + } + ] + }, + { + "file": "suites/compiler-rebuild/fixtures/aot.ts", + "defaults": { + "tier": 1, + "family": "aot", + "implementation": "effect-aot", + "astTags": [ + "Objects", + "String", + "Number", + "Boolean" + ], + "operation": "decode", + "path": "valid", + "size": 3 + }, + "cases": [ + { + "name": "aot-parseValid", + "export": "parseValid", + "scenario": "compiler-rebuild-parseValid" + }, + { + "name": "aot-parseExtra", + "export": "parseExtra", + "scenario": "compiler-rebuild-parseExtra", + "size": 4 + }, + { + "name": "aot-parseInvalid", + "export": "parseInvalid", + "scenario": "compiler-rebuild-parseInvalid", + "path": "invalid" + }, + { + "name": "aot-isValid", + "export": "isValid", + "scenario": "compiler-rebuild-isValid", + "operation": "is" + }, + { + "name": "aot-isInvalid", + "export": "isInvalid", + "scenario": "compiler-rebuild-isInvalid", + "operation": "is", + "path": "invalid" + }, + { + "name": "aot-encode", + "export": "encode", + "scenario": "compiler-rebuild-encode", + "operation": "encode" + }, + { + "name": "aot-strict", + "export": "strict", + "scenario": "compiler-rebuild-strict" + }, + { + "name": "aot-nested", + "export": "nested", + "scenario": "compiler-rebuild-nested", + "astTags": [ + "Objects", + "Arrays", + "String", + "Number", + "Boolean" + ], + "size": 2 + }, + { + "name": "aot-array", + "export": "array", + "scenario": "compiler-rebuild-array", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "aot-arrayInvalid", + "export": "arrayInvalid", + "scenario": "compiler-rebuild-arrayInvalid", + "path": "invalid", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 33 + }, + { + "name": "aot-tuple", + "export": "tuple", + "scenario": "compiler-rebuild-tuple", + "astTags": [ + "Arrays", + "String", + "Number", + "Boolean" + ], + "size": 34 + }, + { + "name": "aot-record", + "export": "record", + "scenario": "compiler-rebuild-record", + "size": 32 + }, + { + "name": "aot-recordTransformedKeys", + "export": "recordTransformedKeys", + "scenario": "compiler-rebuild-recordTransformedKeys", + "astTags": [ + "Objects", + "String" + ], + "size": 32 + }, + { + "name": "aot-union", + "export": "union", + "scenario": "compiler-rebuild-union", + "astTags": [ + "Union", + "Objects", + "Literal", + "Number" + ], + "size": 8 + }, + { + "name": "aot-oneOf", + "export": "oneOf", + "scenario": "compiler-rebuild-oneOf", + "astTags": [ + "Union", + "Objects", + "String", + "Number" + ], + "size": 2 + }, + { + "name": "aot-transform", + "export": "transform", + "scenario": "compiler-rebuild-transform", + "astTags": [ + "Objects", + "String", + "Number" + ], + "size": 32 + }, + { + "name": "aot-transformInvalid", + "export": "transformInvalid", + "scenario": "compiler-rebuild-transformInvalid", + "path": "invalid", + "astTags": [ + "String", + "Number" + ], + "size": 1 + }, + { + "name": "aot-rootTransform", + "export": "rootTransform", + "scenario": "compiler-rebuild-rootTransform", + "astTags": [ + "String", + "Number" + ], + "size": 1 + }, + { + "name": "aot-middleware", + "export": "middleware", + "scenario": "compiler-rebuild-middleware" + }, + { + "name": "aot-recursive", + "export": "recursive", + "scenario": "compiler-rebuild-recursive", + "astTags": [ + "Objects", + "Arrays", + "Suspend", + "Number" + ], + "size": 31 + }, + { + "name": "aot-suspendRoot", + "export": "suspendRoot", + "scenario": "compiler-rebuild-suspendRoot", + "astTags": [ + "Suspend", + "Objects", + "Number" + ], + "size": 1 + }, + { + "name": "aot-declaration", + "export": "declaration", + "scenario": "compiler-rebuild-declaration", + "astTags": [ + "Declaration", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "aot-makeStruct", + "export": "makeStruct", + "scenario": "compiler-rebuild-makeStruct", + "operation": "make", + "astTags": [ + "Objects", + "Number" + ], + "size": 32 + }, + { + "name": "aot-makeArray", + "export": "makeArray", + "scenario": "compiler-rebuild-makeArray", + "operation": "make", + "astTags": [ + "Arrays", + "Objects", + "String", + "Number", + "Boolean" + ], + "size": 32 + }, + { + "name": "aot-makeUnion", + "export": "makeUnion", + "scenario": "compiler-rebuild-makeUnion", + "operation": "make", + "astTags": [ + "Union", + "Objects", + "Literal", + "Number" + ], + "size": 8 + } + ] + }, + { + "file": "suites/compiler-rebuild/fixtures/valibot.ts", + "defaults": { + "tier": 1, + "family": "valibot", + "implementation": "valibot", + "astTags": [], + "operation": "decode", + "path": "valid", + "size": 3 + }, + "cases": [ + { + "name": "valibot-parseValid", + "export": "parseValid", + "scenario": "compiler-rebuild-parseValid" + }, + { + "name": "valibot-parseInvalid", + "export": "parseInvalid", + "scenario": "compiler-rebuild-parseInvalid", + "path": "invalid" + }, + { + "name": "valibot-isValid", + "export": "isValid", + "scenario": "compiler-rebuild-isValid", + "operation": "is" + }, + { + "name": "valibot-isInvalid", + "export": "isInvalid", + "scenario": "compiler-rebuild-isInvalid", + "operation": "is", + "path": "invalid" + }, + { + "name": "valibot-array", + "export": "array", + "scenario": "compiler-rebuild-array", + "size": 32 + }, + { + "name": "valibot-record", + "export": "record", + "scenario": "compiler-rebuild-record", + "size": 32 + }, + { + "name": "valibot-recordTransformedKeys", + "export": "recordTransformedKeys", + "scenario": "compiler-rebuild-recordTransformedKeys", + "size": 32 + }, + { + "name": "valibot-union", + "export": "union", + "scenario": "compiler-rebuild-union", + "size": 8 + }, + { + "name": "valibot-transform", + "export": "transform", + "scenario": "compiler-rebuild-transform", + "size": 32 + }, + { + "name": "valibot-makeStruct", + "export": "makeStruct", + "scenario": "compiler-rebuild-makeStruct", + "operation": "make", + "size": 32 + }, + { + "name": "valibot-makeArray", + "export": "makeArray", + "scenario": "compiler-rebuild-makeArray", + "operation": "make", + "size": 32 + } + ] + }, + { + "file": "suites/compiler-rebuild/fixtures/zod-jitless.ts", + "defaults": { + "tier": 1, + "family": "zod-jitless", + "implementation": "zod4-jitless", + "astTags": [], + "operation": "decode", + "path": "valid", + "size": 3 + }, + "cases": [ + { + "name": "zod-jitless-parseValid", + "export": "parseValid", + "scenario": "compiler-rebuild-parseValid" + }, + { + "name": "zod-jitless-parseInvalid", + "export": "parseInvalid", + "scenario": "compiler-rebuild-parseInvalid", + "path": "invalid" + }, + { + "name": "zod-jitless-isValid", + "export": "isValid", + "scenario": "compiler-rebuild-isValid", + "operation": "is" + }, + { + "name": "zod-jitless-isInvalid", + "export": "isInvalid", + "scenario": "compiler-rebuild-isInvalid", + "operation": "is", + "path": "invalid" + }, + { + "name": "zod-jitless-array", + "export": "array", + "scenario": "compiler-rebuild-array", + "size": 32 + }, + { + "name": "zod-jitless-record", + "export": "record", + "scenario": "compiler-rebuild-record", + "size": 32 + }, + { + "name": "zod-jitless-recordTransformedKeys", + "export": "recordTransformedKeys", + "scenario": "compiler-rebuild-recordTransformedKeys", + "size": 32 + }, + { + "name": "zod-jitless-union", + "export": "union", + "scenario": "compiler-rebuild-union", + "size": 8 + }, + { + "name": "zod-jitless-transform", + "export": "transform", + "scenario": "compiler-rebuild-transform", + "size": 32 + }, + { + "name": "zod-jitless-makeStruct", + "export": "makeStruct", + "scenario": "compiler-rebuild-makeStruct", + "operation": "make", + "size": 32 + }, + { + "name": "zod-jitless-makeArray", + "export": "makeArray", + "scenario": "compiler-rebuild-makeArray", + "operation": "make", + "size": 32 + } + ] + }, + { + "file": "suites/compiler-rebuild/fixtures/zod-compiled.ts", + "defaults": { + "tier": 1, + "family": "zod-compiled", + "implementation": "zod4-compiled", + "astTags": [], + "operation": "decode", + "path": "valid", + "size": 3 + }, + "cases": [ + { + "name": "zod-compiled-parseValid", + "export": "parseValid", + "scenario": "compiler-rebuild-parseValid" + }, + { + "name": "zod-compiled-parseExtra", + "export": "parseExtra", + "scenario": "compiler-rebuild-parseExtra", + "size": 4 + }, + { + "name": "zod-compiled-parseInvalid", + "export": "parseInvalid", + "scenario": "compiler-rebuild-parseInvalid", + "path": "invalid" + }, + { + "name": "zod-compiled-isValid", + "export": "isValid", + "scenario": "compiler-rebuild-isValid", + "operation": "is" + }, + { + "name": "zod-compiled-isInvalid", + "export": "isInvalid", + "scenario": "compiler-rebuild-isInvalid", + "operation": "is", + "path": "invalid" + }, + { + "name": "zod-compiled-encode", + "export": "encode", + "scenario": "compiler-rebuild-encode", + "operation": "encode" + }, + { + "name": "zod-compiled-strict", + "export": "strict", + "scenario": "compiler-rebuild-strict" + }, + { + "name": "zod-compiled-nested", + "export": "nested", + "scenario": "compiler-rebuild-nested", + "size": 2 + }, + { + "name": "zod-compiled-array", + "export": "array", + "scenario": "compiler-rebuild-array", + "size": 32 + }, + { + "name": "zod-compiled-arrayInvalid", + "export": "arrayInvalid", + "scenario": "compiler-rebuild-arrayInvalid", + "path": "invalid", + "size": 33 + }, + { + "name": "zod-compiled-record", + "export": "record", + "scenario": "compiler-rebuild-record", + "size": 32 + }, + { + "name": "zod-compiled-recordTransformedKeys", + "export": "recordTransformedKeys", + "scenario": "compiler-rebuild-recordTransformedKeys", + "size": 32 + }, + { + "name": "zod-compiled-union", + "export": "union", + "scenario": "compiler-rebuild-union", + "size": 8 + }, + { + "name": "zod-compiled-transform", + "export": "transform", + "scenario": "compiler-rebuild-transform", + "size": 32 + }, + { + "name": "zod-compiled-transformInvalid", + "export": "transformInvalid", + "scenario": "compiler-rebuild-transformInvalid", + "path": "invalid", + "size": 1 + }, + { + "name": "zod-compiled-declaration", + "export": "declaration", + "scenario": "compiler-rebuild-declaration", + "size": 32 + }, + { + "name": "zod-compiled-makeStruct", + "export": "makeStruct", + "scenario": "compiler-rebuild-makeStruct", + "operation": "make", + "size": 32 + }, + { + "name": "zod-compiled-makeArray", + "export": "makeArray", + "scenario": "compiler-rebuild-makeArray", + "operation": "make", + "size": 32 + }, + { + "name": "zod-compiled-makeUnion", + "export": "makeUnion", + "scenario": "compiler-rebuild-makeUnion", + "operation": "make", + "size": 8 + } + ] + } + ] } ] } diff --git a/repos/effect/packages/effect/runtimeperf/run.mts b/repos/effect/packages/effect/runtimeperf/run.mts index b97665f60..3cac08601 100644 --- a/repos/effect/packages/effect/runtimeperf/run.mts +++ b/repos/effect/packages/effect/runtimeperf/run.mts @@ -18,7 +18,11 @@ import { relativeToRepo, reportPath, resolveDefaults, + runPath, + scenarioBatchSize, selectFixtures, + statsPath, + utilsPath, workerPath, writeJson } from "./utils.mts" @@ -31,7 +35,7 @@ Options: --warmup-time --tier <0-3> --family - --implementation + --implementation ` const rotate = (items, offset) => items.map((_, index) => items[(index + offset) % items.length]) @@ -52,12 +56,12 @@ const main = () => { for (const [scenario, group] of groups) { const calibrations = new Map(group.map((fixture) => [fixture, calibrateFixture(fixture, defaults)])) + const batchSize = scenarioBatchSize(group, calibrations) 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) + const measurement = measureFixture(fixture, defaults, batchSize) byTarget.get(fixture.target).push(measurement) executionOrder.push({ scenario, round: round + 1, target: fixture.target }) } @@ -68,7 +72,7 @@ const main = () => { const measurements = byTarget.get(fixture.target) results.push({ fixture, - batchSize: calibration.batchSize, + batchSize, calibration, measurements, aggregate: aggregateMeasurements(measurements) @@ -81,7 +85,7 @@ const main = () => { const effect = group.find((result) => result.fixture.implementation === "effect") if (!effect) continue for (const candidate of group) { - if (candidate === effect) continue + if (candidate.fixture.implementation === "effect") continue crossLibrary.push({ scenario, implementation: candidate.fixture.implementation, @@ -116,16 +120,14 @@ const main = () => { 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), + run: hashFile(runPath), + stats: hashFile(statsPath), + utils: hashFile(utilsPath), worker: hashFile(workerPath), fixtures: Object.fromEntries( [...new Set(selected.map((fixture) => fixture.fixturePath))] @@ -140,11 +142,12 @@ const main = () => { writeJson(path, report) const comparisons = new Map(crossLibrary.map((item) => [`${item.scenario}/${item.implementation}`, item])) printTable( - ["scenario", "implementation", "ns/op", "mad", "vs Effect"], + ["scenario", "family", "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.family, result.fixture.implementation, formatNs(result.aggregate.median), formatNs(result.aggregate.mad), diff --git a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts index 374da26c4..5418e34f3 100644 --- a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts +++ b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts @@ -376,6 +376,33 @@ export const checkPass100 = () => { } } +export const arrayCheckPass100 = () => { + const property = FastCheck.property(FastCheck.array(FastCheck.integer(), { maxLength: 50 }), () => true) + return { + run: () => FastCheck.check(property, { numRuns: 100, seed }), + validate: (result: FastCheck.RunDetails<[Array]>) => { + assert.equal(result.failed, false) + assert.equal(result.numRuns, 100) + assert.equal(result.numSkips, 0) + } + } +} + +export const arrayCheckFalsifyAndShrink = () => { + const arbitrary = FastCheck.array(FastCheck.constantFrom(8, 27, 0, 1), { maxLength: 4 }) + const property = FastCheck.property( + arbitrary, + (values) => !(values.includes(27) && values.indexOf(0) > values.indexOf(27)) + ) + return { + run: () => FastCheck.check(property, { examples: [[[8, 27, 0, 1]]], numRuns: 1, seed }), + validate: (result: FastCheck.RunDetails<[Array]>) => { + assert.equal(result.failed, true) + assert.deepEqual(result.counterexample, [[27, 0]]) + } + } +} + export const testSchemaVerifyGeneration100 = () => ({ run: () => { const schema = Schema.Int diff --git a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts index 79d46504b..99a70d9dd 100644 --- a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts +++ b/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts @@ -391,6 +391,34 @@ export const checkPass100 = () => { } } +export const arrayCheckPass100 = () => { + const arbitrary = Arbitrary.schema(Schema.Array(Schema.Int)) + const program = Arbitrary.checkEffect(arbitrary, () => true, { runs: 100, seed, size: 50 }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult, never>) => { + assert.deepEqual(result, { _tag: "Passed", runs: 100, discards: 0 }) + } + } +} + +export const arrayCheckFalsifyAndShrink = () => { + const arbitrary = Arbitrary.schema(Schema.Array(Schema.Literals([8, 27, 0, 1]))) + const property = (values: ReadonlyArray) => !(values.includes(27) && values.indexOf(0) > values.indexOf(27)) + const program = Arbitrary.checkEffect(arbitrary, property, { runs: 1, seed: 1967, size: 4 }) + return { + run: () => Effect.runSync(program), + validate: (result: Arbitrary.CheckResult, never>) => { + assert.equal(result._tag, "Falsified") + if (result._tag !== "Falsified") return + assert.deepEqual(result.initialInput, [8, 27, 0, 1]) + // Both revisions must preserve the failure; the regression test checks the new, smaller result. + assert.equal(property(result.shrunkInput), false) + assert.ok(result.shrunkInput.length <= 3) + } + } +} + export const testSchemaVerifyGeneration100 = () => { const asserts = new TestSchema.Asserts(Schema.Int) return { diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/README.md b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/README.md new file mode 100644 index 000000000..25997e2c2 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/README.md @@ -0,0 +1,81 @@ +# Compiler rebuild comparison + +The `compiler-rebuild` suite measures 25 public SchemaParser operations with the +interpreter, selective JIT and generated AOT modules. Nineteen cases also run +against `z.compile(schema, { strict: true })`; eleven representative cases run +against Valibot and Zod with `{ jitless: true }`. The fixture families name the +execution mode. Cases +cover simple and nested Structs, Arrays, tuples, Records, transformed Record +keys, anyOf/oneOf Unions, transformations, middleware, recursive and suspended +schemas, Declarations, construction, +and successful and failing validation. + +The Effect tuple has a trailing element after its rest element, Zod compilation +does not support recursive schemas or `z.xor`, and Zod has no equivalent +decoding middleware. Those four cases intentionally have no Zod fixture. Zod +compilation only accelerates forward parsing, so the encode case measures its +documented runtime fallback. Zod parsing stands in for Effect construction +because Zod has no separate constructor operation. Valibot parsing with defaults +is used for the same comparison. + +```sh +pnpm runtimeperf-compare compiler-rebuild --base schema-compiler +pnpm runtimeperf-compare compiler-rebuild --base main --family interpreted +pnpm runtimeperf compiler-rebuild +``` + +The first command compares the current working tree with the archived compiler +branch. The second isolates changes to interpreted parsing against main. Schema +and parser construction are outside the steady-state measurements. AOT fixtures +generate their version-specific module in a separate process before loading it. + +The standard paired harness records five alternating rounds, validates every +fixture before and after measurement, uses a common batch within each pair, and +reports bootstrap confidence intervals. Cross-mode rankings are descriptive; +the paired base/head comparison is the regression evidence. + +## Retained heap and first use + +For a current cross-library comparison of retained heap, V8 code memory, peak +RSS and preparation CPU, run: + +```sh +node packages/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts \ + tmp/schema-compiler-resources.json 5 100,500 +node packages/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts \ + tmp/schema-compiler-resources.json +``` + +Each sample runs in a fresh process. The report calculates median per-schema +slopes between 100 and 500 distinct schemas, which removes fixed module and +lazy initialization costs. Fixture inputs are released before retained-memory +measurement. Preparation CPU includes adapter creation and first use; AOT also +includes loading and installing the generated module. AOT source generation is +reported separately because it runs at build time. Hot synchronous CPU is +already represented by the normal runtime measurements. + +The older Effect-only probe below remains available for comparisons with its +existing archived results. + +The standalone cost probe accepts a checkout root, mode, operation, shape and +schema count. Run several fresh processes per combination and alternate the +checkout order. Supported modes are `interpreted`, `jit` and `aot`; operations +are `decode`, `invalid`, `is` and `make`; shapes are `struct`, `array`, `transform` +and `default`. + +```sh +node --expose-gc packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts heap "$PWD" jit decode struct 500 +node --expose-gc packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts cold "$PWD" jit decode struct 500 +``` + +Heap measurements exclude schema construction and module imports, and retain +both schema objects and public parser functions. They include operation setup +and first use. Forced GC removes transient parse output and issues. The AOT +generator runs in a separate process, so its retained heap is not attributed to +the runtime application. + +First-use measurements include constructing a fresh schema, installing its +compiler when requested, creating a public parser and calling it. They exclude +module imports and AOT source generation. Repeated shapes can benefit from V8's +source cache, so this is not process startup latency. Treat these timing probes +as exploratory, separately from the paired throughput results. diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts new file mode 100644 index 000000000..1280c9921 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts @@ -0,0 +1,127 @@ +import assert from "node:assert/strict" +import { execFileSync } from "node:child_process" +import { mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { join } from "node:path" +import { fileURLToPath, pathToFileURL } from "node:url" + +const [command, root, mode, operation, shape = "struct", countString = "500"] = process.argv.slice(2) +const count = Number(countString) +const includes = (values: ReadonlyArray, value: string | undefined) => + value !== undefined && values.includes(value) +if (command !== "heap" && command !== "cold" && command !== "generate") { + throw new Error("command must be heap, cold or generate") +} +if (root === undefined) throw new Error("root is required") +if (!includes(["interpreted", "jit", "aot"], mode)) { + throw new Error("mode must be interpreted, jit or aot") +} +if (!includes(["decode", "invalid", "is", "make"], operation)) { + throw new Error("operation must be decode, invalid, is or make") +} +if (!includes(["struct", "array", "transform", "default"], shape)) { + throw new Error("shape must be struct, array, transform or default") +} +if (!Number.isSafeInteger(count) || count <= 0) throw new Error("count must be a positive integer") +const load = (path: string) => import(pathToFileURL(join(root, "packages/effect/src", path + ".ts")).href) +const Schema = await load("Schema") +const Parser = await load("SchemaParser") +const AST = await load("SchemaAST") +const Effect = await load("Effect") +const create = () => { + const struct = Schema.Struct({ name: Schema.String, age: Schema.Number, active: Schema.Boolean }) + return shape === "array" ? Schema.Array(struct) + : shape === "transform" ? Schema.Struct({ value: Schema.NumberFromString }) + : shape === "default" ? Schema.Struct({ value: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(1))) }) + : struct +} +const valid = shape === "array" ? Array.from({ length: 32 }, () => ({ name: "Ada", age: 37, active: true })) + : shape === "transform" ? { value: "1" } + : shape === "default" ? { value: 1 } + : { name: "Ada", age: 37, active: true } +const typeValid = shape === "transform" ? { value: 1 } : valid +const input = operation === "invalid" ? { name: "Ada", age: "bad", active: true } + : operation === "make" && shape === "default" ? {} + : operation === "make" || operation === "is" ? typeValid + : valid +const expected = shape === "transform" || shape === "default" ? { value: 1 } : valid + +if (command === "generate") { + const AOT = await load("unstable/schema/SchemaAOTCompiler") + const schema = create() + const targetOperation = operation === "make" ? "make" : operation === "is" ? "is" : "decode" + const ast = targetOperation === "decode" ? schema.ast : AST.toType(schema.ast) + writeFileSync(process.argv[8], AOT.compile([{ ast, operations: [targetOperation] }])) +} else { + for (let i = 0; i < 5; i++) globalThis.gc?.() + const beforeImport = process.memoryUsage().heapUsed + let enable: ((ast: unknown) => void) | undefined + let install: ((asts: Array) => void) | undefined + let directory: string | undefined + if (mode === "jit") { + enable = (await load("unstable/schema/SchemaJITCompiler")).enable + } else if (mode === "aot") { + directory = mkdtempSync(join(root, "packages/effect/.compiler-memory-")) + const generated = join(directory, "generated.mjs") + execFileSync(process.execPath, [fileURLToPath(import.meta.url), "generate", root, mode, operation, shape, countString, generated]) + install = (await import(pathToFileURL(generated).href)).install + } + for (let i = 0; i < 5; i++) globalThis.gc?.() + const moduleBytes = process.memoryUsage().heapUsed - beforeImport + const prepare = (schema: any) => { + const ast = operation === "make" || operation === "is" ? AST.toType(schema.ast) : schema.ast + if (enable) enable(ast) + if (install) install([ast]) + return operation === "make" ? Parser.make(schema) + : operation === "is" ? Parser.is(schema) + : Parser.decodeUnknownSync(schema) + } + const run = (parse: (input: unknown) => unknown) => { + try { + return parse(input) + } catch (error) { + if (operation !== "invalid") throw error + return error + } + } + const validate = (value: unknown) => { + if (operation === "invalid") { + assert.ok(value instanceof Error) + assert.equal(value.message, "Schema validation failed") + assert.equal("cause" in value, true) + } + else if (operation === "is") assert.equal(value, true) + else assert.deepEqual(value, expected) + } + try { + for (let i = 0; i < 20; i++) validate(run(prepare(create()))) + if (command === "heap") { + assert.equal(typeof globalThis.gc, "function") + const schemas = Array.from({ length: count }, create) + for (let i = 0; i < 5; i++) globalThis.gc!() + const before = process.memoryUsage().heapUsed + const parsers = schemas.map((schema) => { + const parse = prepare(schema) + validate(run(parse)) + return parse + }) + for (let i = 0; i < 5; i++) globalThis.gc!() + const after = process.memoryUsage().heapUsed + // Keep schemas and adapters alive across the measurement. + assert.equal(parsers.length, count) + assert.equal(schemas.length, count) + process.stdout.write(JSON.stringify({ mode, operation, shape, count, moduleBytes, bytesPerSchema: (after - before) / count })) + } else if (command === "cold") { + const samples = [] + for (let round = 0; round < 7; round++) { + let value: unknown + const start = process.hrtime.bigint() + for (let i = 0; i < count; i++) value = run(prepare(create())) + samples.push(Number(process.hrtime.bigint() - start) / count) + validate(value) + } + process.stdout.write(JSON.stringify({ mode, operation, shape, count, nsPerSchema: samples })) + } + } finally { + if (directory) rmSync(directory, { recursive: true, force: true }) + } +} diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/aot.ts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/aot.ts new file mode 100644 index 000000000..cb95c7d3f --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/aot.ts @@ -0,0 +1,39 @@ +import { execFileSync } from "node:child_process" +import { mkdtempSync, rmSync } from "node:fs" +import { join } from "node:path" +import { fileURLToPath, pathToFileURL } from "node:url" +import { fixture, roots } from "./cases.ts" +const directory = mkdtempSync(fileURLToPath(new URL("./.aot-", import.meta.url))) +try { + const file = join(directory, "generated.mjs") + execFileSync(process.execPath, [fileURLToPath(new URL("./generate.mts", import.meta.url)), file]) + const generated = await import(pathToFileURL(file).href) + generated.install(roots) +} finally { + rmSync(directory, { recursive: true, force: true }) +} +export const parseValid = () => fixture("parseValid") +export const parseExtra = () => fixture("parseExtra") +export const parseInvalid = () => fixture("parseInvalid") +export const isValid = () => fixture("isValid") +export const isInvalid = () => fixture("isInvalid") +export const encode = () => fixture("encode") +export const strict = () => fixture("strict") +export const nested = () => fixture("nested") +export const array = () => fixture("array") +export const arrayInvalid = () => fixture("arrayInvalid") +export const tuple = () => fixture("tuple") +export const record = () => fixture("record") +export const recordTransformedKeys = () => fixture("recordTransformedKeys") +export const union = () => fixture("union") +export const oneOf = () => fixture("oneOf") +export const transform = () => fixture("transform") +export const transformInvalid = () => fixture("transformInvalid") +export const rootTransform = () => fixture("rootTransform") +export const middleware = () => fixture("middleware") +export const recursive = () => fixture("recursive") +export const suspendRoot = () => fixture("suspendRoot") +export const declaration = () => fixture("declaration") +export const makeStruct = () => fixture("makeStruct") +export const makeArray = () => fixture("makeArray") +export const makeUnion = () => fixture("makeUnion") diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/cases.ts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/cases.ts new file mode 100644 index 000000000..16e22ae4f --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/cases.ts @@ -0,0 +1,143 @@ +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import * as SchemaAST from "effect/SchemaAST" +import * as SchemaParser from "effect/SchemaParser" +import * as SchemaTransformation from "effect/SchemaTransformation" +import assert from "node:assert/strict" + +export const person = () => Schema.Struct({ name: Schema.String, age: Schema.Number, active: Schema.Boolean }) +export const input = { name: "Ada", age: 37, active: true } +const small = person() +const nested = Schema.Struct({ user: person(), tags: Schema.Array(Schema.String) }) +const array = Schema.Array(person()) +const arrayInput = Array.from({ length: 32 }, () => ({ ...input })) +const tuple = Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean]) +const tupleInput = ["head", ...Array.from({ length: 32 }, (_, i) => i), true] +const record = Schema.Record(Schema.String, person()) +const recordInput = Object.fromEntries(arrayInput.map((value, i) => [String(i), value])) +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}`]) +) +const transformedKeyRecordOutput = Object.fromEntries( + Array.from({ length: 32 }, (_, index) => [`field${index}Value`, `value${index}`]) +) +const union = Schema.Union( + Array.from({ length: 8 }, (_, i) => Schema.Struct({ tag: Schema.Literal(i), value: Schema.Number })) +) +const oneOf = Schema.Union([Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number })], { + mode: "oneOf" +}) +const fields = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, Schema.NumberFromString])) +const transformed = Schema.Struct(fields) +const transformedInput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, String(i)])) +const transformedOutput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, i])) +const checkedTransform = Schema.String.pipe( + Schema.decodeTo( + Schema.Number.check(Schema.isGreaterThan(0)), + SchemaTransformation.transform({ decode: Number, encode: String }) + ) +) +const rootTransform = Schema.FiniteFromString +const middleware = small.pipe(Schema.middlewareDecoding((effect) => effect)) +const declaration = Schema.ReadonlySet(person()) +const declarationInput = new Set(arrayInput) +interface Node { + readonly value: number + readonly children: ReadonlyArray +} +const recursive: Schema.Codec = Schema.Struct({ + value: Schema.Number, + children: Schema.Array(Schema.suspend(() => recursive)) +}) +const node = (depth: number): Node => ({ + value: depth, + children: depth === 0 ? [] : [node(depth - 1), node(depth - 1)] +}) +const recursiveInput = node(4) +const suspendRoot = Schema.suspend(() => Schema.Struct({ value: Schema.Number })) +const defaults = Schema.Struct( + Object.fromEntries( + Array.from( + { length: 32 }, + (_, i) => [`v${i}`, Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(i)))] + ) + ) +) + +type Case = { + schema: Schema.Constraint + input: unknown + expected: unknown + operation?: "is" | "make" | "encode" + invalid?: boolean + options?: SchemaAST.ParseOptions +} + +export const cases: Record = { + parseValid: { schema: small, input, expected: input }, + parseExtra: { schema: small, input: { ...input, extra: 1 }, expected: input }, + parseInvalid: { schema: small, input: { ...input, age: "bad" }, expected: true, invalid: true }, + isValid: { schema: small, input, expected: true, operation: "is" }, + isInvalid: { schema: small, input: { ...input, age: "bad" }, expected: false, operation: "is" }, + encode: { schema: small, input, expected: input, operation: "encode" }, + strict: { schema: small, input, expected: input, options: { onExcessProperty: "error" } }, + nested: { schema: nested, input: { user: input, tags: ["a", "b"] }, expected: { user: input, tags: ["a", "b"] } }, + array: { schema: array, input: arrayInput, expected: arrayInput }, + arrayInvalid: { schema: array, input: [...arrayInput, { ...input, age: "bad" }], expected: true, invalid: true }, + tuple: { schema: tuple, input: tupleInput, expected: tupleInput }, + record: { schema: record, input: recordInput, expected: recordInput }, + recordTransformedKeys: { + schema: transformedKeyRecord, + input: transformedKeyRecordInput, + expected: transformedKeyRecordOutput + }, + union: { schema: union, input: { tag: 7, value: 1 }, expected: { tag: 7, value: 1 } }, + oneOf: { schema: oneOf, input: { b: 1 }, expected: { b: 1 } }, + transform: { schema: transformed, input: transformedInput, expected: transformedOutput }, + transformInvalid: { schema: checkedTransform, input: "-1", expected: true, invalid: true }, + rootTransform: { schema: rootTransform, input: "123", expected: 123 }, + middleware: { schema: middleware, input, expected: input }, + recursive: { schema: recursive, input: recursiveInput, expected: recursiveInput }, + suspendRoot: { schema: suspendRoot, input: { value: 1 }, expected: { value: 1 } }, + declaration: { schema: declaration, input: declarationInput, expected: declarationInput }, + makeStruct: { schema: defaults, input: {}, expected: transformedOutput, operation: "make" }, + makeArray: { schema: array, input: arrayInput, expected: arrayInput, operation: "make" }, + makeUnion: { schema: union, input: { tag: 7, value: 1 }, expected: { tag: 7, value: 1 }, operation: "make" } +} + +export const targets = Object.values(cases).map(({ operation, schema }) => + operation === "encode" + ? { ast: SchemaAST.flip(schema.ast), operations: ["decode"] as const } + : operation === "is" || operation === "make" + ? { ast: SchemaAST.toType(schema.ast), operations: [operation] as const } + : { ast: schema.ast, operations: ["decode"] as const } +) +export const roots = targets.map((target) => target.ast) + +export const fixture = (name: string) => { + const { schema, input, expected, operation, invalid, options } = cases[name] + const parse = operation === "is" ? + SchemaParser.is(schema) + : operation === "make" ? + SchemaParser.make(schema) + : operation === "encode" ? + SchemaParser.encodeUnknownSync(schema as Schema.ConstraintEncoder) + : SchemaParser.decodeUnknownSync(schema as Schema.ConstraintDecoder, options) + return { + run: invalid ? + () => { + try { + parse(input) + return false + } catch { + return true + } + } : + () => parse(input), + validate: (result: unknown) => assert.deepEqual(result, expected) + } +} diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/generate.mts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/generate.mts new file mode 100644 index 000000000..defdd093e --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/generate.mts @@ -0,0 +1,5 @@ +import { writeFileSync } from "node:fs" +import { compile } from "effect/unstable/schema/SchemaAOTCompiler" +import { targets } from "./cases.ts" + +writeFileSync(process.argv[2], compile(targets)) diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/interpreted.ts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/interpreted.ts new file mode 100644 index 000000000..8412c30a9 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/interpreted.ts @@ -0,0 +1,26 @@ +import { fixture } from "./cases.ts" +export const parseValid = () => fixture("parseValid") +export const parseExtra = () => fixture("parseExtra") +export const parseInvalid = () => fixture("parseInvalid") +export const isValid = () => fixture("isValid") +export const isInvalid = () => fixture("isInvalid") +export const encode = () => fixture("encode") +export const strict = () => fixture("strict") +export const nested = () => fixture("nested") +export const array = () => fixture("array") +export const arrayInvalid = () => fixture("arrayInvalid") +export const tuple = () => fixture("tuple") +export const record = () => fixture("record") +export const recordTransformedKeys = () => fixture("recordTransformedKeys") +export const union = () => fixture("union") +export const oneOf = () => fixture("oneOf") +export const transform = () => fixture("transform") +export const transformInvalid = () => fixture("transformInvalid") +export const rootTransform = () => fixture("rootTransform") +export const middleware = () => fixture("middleware") +export const recursive = () => fixture("recursive") +export const suspendRoot = () => fixture("suspendRoot") +export const declaration = () => fixture("declaration") +export const makeStruct = () => fixture("makeStruct") +export const makeArray = () => fixture("makeArray") +export const makeUnion = () => fixture("makeUnion") diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/jit.ts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/jit.ts new file mode 100644 index 000000000..8c4894735 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/jit.ts @@ -0,0 +1,28 @@ +import { enable } from "effect/unstable/schema/SchemaJITCompiler" +import { fixture, roots } from "./cases.ts" +for (const ast of roots) enable(ast) +export const parseValid = () => fixture("parseValid") +export const parseExtra = () => fixture("parseExtra") +export const parseInvalid = () => fixture("parseInvalid") +export const isValid = () => fixture("isValid") +export const isInvalid = () => fixture("isInvalid") +export const encode = () => fixture("encode") +export const strict = () => fixture("strict") +export const nested = () => fixture("nested") +export const array = () => fixture("array") +export const arrayInvalid = () => fixture("arrayInvalid") +export const tuple = () => fixture("tuple") +export const record = () => fixture("record") +export const recordTransformedKeys = () => fixture("recordTransformedKeys") +export const union = () => fixture("union") +export const oneOf = () => fixture("oneOf") +export const transform = () => fixture("transform") +export const transformInvalid = () => fixture("transformInvalid") +export const rootTransform = () => fixture("rootTransform") +export const middleware = () => fixture("middleware") +export const recursive = () => fixture("recursive") +export const suspendRoot = () => fixture("suspendRoot") +export const declaration = () => fixture("declaration") +export const makeStruct = () => fixture("makeStruct") +export const makeArray = () => fixture("makeArray") +export const makeUnion = () => fixture("makeUnion") diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/valibot.ts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/valibot.ts new file mode 100644 index 000000000..e729d5fff --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/valibot.ts @@ -0,0 +1,93 @@ +import * as Str from "effect/String" +import assert from "node:assert/strict" +import * as v from "valibot" + +const person = () => v.object({ name: v.string(), age: v.number(), active: v.boolean() }) +const input = { name: "Ada", age: 37, active: true } +const small = person() +const arraySchema = v.array(person()) +const arrayInput = Array.from({ length: 32 }, () => ({ ...input })) +const recordSchema = v.record(v.string(), person()) +const recordInput = Object.fromEntries(arrayInput.map((value, i) => [String(i), value])) +const transformedKeyRecordSchema = v.record( + v.pipe(v.string(), v.transform(Str.snakeToCamel)), + v.string() +) +const transformedKeyRecordInput = Object.fromEntries( + Array.from({ length: 32 }, (_, i) => [`field_${i}_value`, String(i)]) +) +const transformedKeyRecordOutput = Object.fromEntries( + Array.from({ length: 32 }, (_, i) => [`field${i}Value`, String(i)]) +) +const unionSchema = v.variant( + "tag", + Array.from({ length: 8 }, (_, i) => v.object({ tag: v.literal(i), value: v.number() })) +) +const transformedSchema = v.object( + Object.fromEntries( + Array.from({ length: 32 }, (_, i) => [`v${i}`, v.pipe(v.string(), v.transform(Number))]) + ) +) +const transformedInput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, String(i)])) +const transformedOutput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, i])) +const defaultsSchema = v.object( + Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, v.optional(v.number(), i)])) +) + +type Case = { + readonly schema: v.BaseSchema> + readonly input: unknown + readonly expected: unknown + readonly operation?: "is" | "make" + readonly invalid?: boolean +} + +const cases: Record = { + parseValid: { schema: small, input, expected: input }, + parseInvalid: { schema: small, input: { ...input, age: "bad" }, expected: true, invalid: true }, + isValid: { schema: small, input, expected: true, operation: "is" }, + isInvalid: { schema: small, input: { ...input, age: "bad" }, expected: false, operation: "is" }, + array: { schema: arraySchema, input: arrayInput, expected: arrayInput }, + record: { schema: recordSchema, input: recordInput, expected: recordInput }, + recordTransformedKeys: { + schema: transformedKeyRecordSchema, + input: transformedKeyRecordInput, + expected: transformedKeyRecordOutput + }, + union: { schema: unionSchema, input: { tag: 7, value: 1 }, expected: { tag: 7, value: 1 } }, + transform: { schema: transformedSchema, input: transformedInput, expected: transformedOutput }, + makeStruct: { schema: defaultsSchema, input: {}, expected: transformedOutput, operation: "make" }, + makeArray: { schema: arraySchema, input: arrayInput, expected: arrayInput, operation: "make" } +} + +const fixture = (name: string) => { + const { schema, input, expected, operation, invalid } = cases[name] + const parse = operation === "is" + ? (input: unknown) => v.is(schema, input) + : (input: unknown) => v.parse(schema, input) + return { + run: invalid + ? () => { + try { + parse(input) + return false + } catch { + return true + } + } + : () => parse(input), + validate: (result: unknown) => assert.deepEqual(result, expected) + } +} + +export const parseValid = () => fixture("parseValid") +export const parseInvalid = () => fixture("parseInvalid") +export const isValid = () => fixture("isValid") +export const isInvalid = () => fixture("isInvalid") +export const array = () => fixture("array") +export const record = () => fixture("record") +export const recordTransformedKeys = () => fixture("recordTransformedKeys") +export const union = () => fixture("union") +export const transform = () => fixture("transform") +export const makeStruct = () => fixture("makeStruct") +export const makeArray = () => fixture("makeArray") diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-cases.ts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-cases.ts new file mode 100644 index 000000000..b2e271b59 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-cases.ts @@ -0,0 +1,133 @@ +import * as Str from "effect/String" +import assert from "node:assert/strict" +import * as z from "zod/v4" + +const person = () => z.object({ name: z.string(), age: z.number(), active: z.boolean() }) +const input = { name: "Ada", age: 37, active: true } +const small = person() +const nestedSchema = z.object({ user: person(), tags: z.array(z.string()) }) +const arraySchema = z.array(person()) +const arrayInput = Array.from({ length: 32 }, () => ({ ...input })) +const recordSchema = z.record(z.string(), person()) +const recordInput = Object.fromEntries(arrayInput.map((value, i) => [String(i), value])) +const transformedKeyRecordSchema = z.record( + z.string().overwrite(Str.snakeToCamel), + z.string() +) +const transformedKeyRecordInput = Object.fromEntries( + Array.from({ length: 32 }, (_, i) => [`field_${i}_value`, String(i)]) +) +const transformedKeyRecordOutput = Object.fromEntries( + Array.from({ length: 32 }, (_, i) => [`field${i}Value`, String(i)]) +) +const unionSchema = z.union( + Array.from({ length: 8 }, (_, i) => z.object({ tag: z.literal(i), value: z.number() })) as [ + z.ZodObject, + z.ZodObject, + ...Array + ] +) +const fields = Object.fromEntries( + Array.from({ length: 32 }, (_, i) => [`v${i}`, z.string().transform(Number)]) +) +const transformedSchema = z.object(fields) +const transformedInput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, String(i)])) +const transformedOutput = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, i])) +const checkedTransform = z.string().transform(Number).pipe(z.number().positive()) +const declarationSchema = z.set(person()) +const declarationInput = new Set(arrayInput) +const defaultsSchema = z.object( + Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`v${i}`, z.number().default(i)])) +) + +type Case = { + readonly schema: z.ZodType + readonly input: unknown + readonly expected: unknown + readonly operation?: "is" | "make" | "encode" + readonly invalid?: boolean +} + +const cases: Record = { + parseValid: { schema: small, input, expected: input }, + parseExtra: { schema: small, input: { ...input, extra: 1 }, expected: input }, + parseInvalid: { schema: small, input: { ...input, age: "bad" }, expected: true, invalid: true }, + isValid: { schema: small, input, expected: true, operation: "is" }, + isInvalid: { schema: small, input: { ...input, age: "bad" }, expected: false, operation: "is" }, + encode: { schema: small, input, expected: input, operation: "encode" }, + strict: { + schema: z.strictObject({ name: z.string(), age: z.number(), active: z.boolean() }), + input, + expected: input + }, + nested: { + schema: nestedSchema, + input: { user: input, tags: ["a", "b"] }, + expected: { user: input, tags: ["a", "b"] } + }, + array: { schema: arraySchema, input: arrayInput, expected: arrayInput }, + arrayInvalid: { + schema: arraySchema, + input: [...arrayInput, { ...input, age: "bad" }], + expected: true, + invalid: true + }, + record: { schema: recordSchema, input: recordInput, expected: recordInput }, + recordTransformedKeys: { + schema: transformedKeyRecordSchema, + input: transformedKeyRecordInput, + expected: transformedKeyRecordOutput + }, + union: { schema: unionSchema, input: { tag: 7, value: 1 }, expected: { tag: 7, value: 1 } }, + transform: { schema: transformedSchema, input: transformedInput, expected: transformedOutput }, + transformInvalid: { schema: checkedTransform, input: "-1", expected: true, invalid: true }, + declaration: { schema: declarationSchema, input: declarationInput, expected: declarationInput }, + makeStruct: { schema: defaultsSchema, input: {}, expected: transformedOutput, operation: "make" }, + makeArray: { schema: arraySchema, input: arrayInput, expected: arrayInput, operation: "make" }, + makeUnion: { + schema: unionSchema, + input: { tag: 7, value: 1 }, + expected: { tag: 7, value: 1 }, + operation: "make" + } +} + +const makeFixture = ( + { input, expected, invalid }: Case, + parse: (input: unknown) => unknown +) => { + return { + run: invalid ? + () => { + try { + parse(input) + return false + } catch { + return true + } + } : + () => parse(input), + validate: (result: unknown) => assert.deepEqual(result, expected) + } +} + +export const compiledFixture = (name: string) => { + const value = cases[name] + const schema = z.compile(value.schema, { strict: true }) + const parse = value.operation === "is" ? + (input: unknown) => z.validate(schema, input) + : value.operation === "encode" ? + (input: unknown) => z.encode(schema, input) + : (input: unknown) => schema.parse(input) + return makeFixture(value, parse) +} + +export const jitlessFixture = (name: string) => { + const value = cases[name] + const parse = value.operation === "is" ? + (input: unknown) => z.validate(value.schema, input, { jitless: true }) + : value.operation === "encode" ? + (input: unknown) => z.encode(value.schema, input, { jitless: true }) + : (input: unknown) => value.schema.parse(input, { jitless: true }) + return makeFixture(value, parse) +} diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-compiled.ts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-compiled.ts new file mode 100644 index 000000000..4fa0b3f8d --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-compiled.ts @@ -0,0 +1,21 @@ +import { compiledFixture as fixture } from "./zod-cases.ts" + +export const parseValid = () => fixture("parseValid") +export const parseExtra = () => fixture("parseExtra") +export const parseInvalid = () => fixture("parseInvalid") +export const isValid = () => fixture("isValid") +export const isInvalid = () => fixture("isInvalid") +export const encode = () => fixture("encode") +export const strict = () => fixture("strict") +export const nested = () => fixture("nested") +export const array = () => fixture("array") +export const arrayInvalid = () => fixture("arrayInvalid") +export const record = () => fixture("record") +export const recordTransformedKeys = () => fixture("recordTransformedKeys") +export const union = () => fixture("union") +export const transform = () => fixture("transform") +export const transformInvalid = () => fixture("transformInvalid") +export const declaration = () => fixture("declaration") +export const makeStruct = () => fixture("makeStruct") +export const makeArray = () => fixture("makeArray") +export const makeUnion = () => fixture("makeUnion") diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-jitless.ts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-jitless.ts new file mode 100644 index 000000000..fc84ea473 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-jitless.ts @@ -0,0 +1,13 @@ +import { jitlessFixture as fixture } from "./zod-cases.ts" + +export const parseValid = () => fixture("parseValid") +export const parseInvalid = () => fixture("parseInvalid") +export const isValid = () => fixture("isValid") +export const isInvalid = () => fixture("isInvalid") +export const array = () => fixture("array") +export const record = () => fixture("record") +export const recordTransformedKeys = () => fixture("recordTransformedKeys") +export const union = () => fixture("union") +export const transform = () => fixture("transform") +export const makeStruct = () => fixture("makeStruct") +export const makeArray = () => fixture("makeArray") diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts new file mode 100644 index 000000000..99e2aa6f0 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts @@ -0,0 +1,235 @@ +import { readFileSync } from "node:fs" +import { resolve } from "node:path" + +type Sample = { + readonly aotBuild?: { + readonly generateCpu: { readonly cpuMicros: number } + readonly maxRssBytes: number + readonly schemaCpu: { readonly cpuMicros: number } + readonly sourceBytes: number + } + readonly case: string + readonly count: number + readonly cpu: { + readonly aotModule?: { readonly cpuMicros: number } + readonly firstCall: { readonly cpuMicros: number } + readonly prepare: { readonly cpuMicros: number } + } + readonly implementation: string + readonly memory: { + readonly compilerPerSchema: Memory + readonly module: Memory + readonly retainedLibraryPerSchema: Memory + } + readonly round: number +} + +type Memory = { + readonly bytecodeBytes: number + readonly codeBytes: number + readonly externalSourceBytes: number + readonly heapBytes: number + readonly maxRssBytes: number +} + +type Report = { + readonly environment: Record + readonly measurement: { + readonly cases: ReadonlyArray + readonly counts: ReadonlyArray + readonly implementations: ReadonlyArray + readonly rounds: number + } + readonly results: ReadonlyArray +} + +const path = resolve(process.argv[2] ?? "tmp/schema-compiler-resources.json") +const report = JSON.parse(readFileSync(path, "utf8")) as Report +const lowCount = Math.min(...report.measurement.counts) +const highCount = Math.max(...report.measurement.counts) +if (lowCount === highCount) throw new Error("the report needs at least two schema counts") + +const median = (values: ReadonlyArray) => { + const sorted = values.toSorted((a, b) => a - b) + const middle = Math.floor(sorted.length / 2) + return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle] +} + +const samples = (implementation: string, caseName: string) => { + const pairs = [] + for (let round = 0; round < report.measurement.rounds; round++) { + const low = report.results.find((sample) => + sample.round === round && sample.implementation === implementation && sample.case === caseName && + sample.count === lowCount + ) + const high = report.results.find((sample) => + sample.round === round && sample.implementation === implementation && sample.case === caseName && + sample.count === highCount + ) + if (low === undefined || high === undefined) throw new Error(`missing samples for ${implementation}/${caseName}`) + pairs.push({ high, low }) + } + return pairs +} + +const perSchemaSlope = ( + implementation: string, + caseName: string, + value: (sample: Sample) => number +) => median(samples(implementation, caseName).map(({ high, low }) => + (value(high) * highCount - value(low) * lowCount) / (highCount - lowCount))) + +const totalSlope = ( + implementation: string, + caseName: string, + value: (sample: Sample) => number +) => median(samples(implementation, caseName).map(({ high, low }) => + (value(high) - value(low)) / (highCount - lowCount))) + +const heap = (implementation: string, caseName: string) => + perSchemaSlope(implementation, caseName, (sample) => sample.memory.retainedLibraryPerSchema.heapBytes) / 1024 +const code = (implementation: string, caseName: string) => + perSchemaSlope( + implementation, + caseName, + (sample) => { + const memory = sample.memory.retainedLibraryPerSchema + return memory.bytecodeBytes + memory.codeBytes + memory.externalSourceBytes + } + ) / 1024 +const peakRss = (implementation: string, caseName: string) => + perSchemaSlope(implementation, caseName, (sample) => sample.memory.retainedLibraryPerSchema.maxRssBytes) / 1024 +const startupCpu = (implementation: string, caseName: string) => + totalSlope( + implementation, + caseName, + (sample) => + sample.cpu.prepare.cpuMicros + sample.cpu.firstCall.cpuMicros + + (implementation === "effect-aot" ? sample.cpu.aotModule?.cpuMicros ?? 0 : 0) + ) + +const fixedDelta = ( + left: string, + right: string, + value: (sample: Sample) => number +) => { + const differences = [] + for (const caseName of report.measurement.cases) { + for (let round = 0; round < report.measurement.rounds; round++) { + for (const count of report.measurement.counts) { + const find = (implementation: string) => report.results.find((sample) => + sample.implementation === implementation && sample.case === caseName && sample.round === round && + sample.count === count + )! + differences.push(value(find(left)) - value(find(right))) + } + } + } + return median(differences) +} + +const names: Record = { + "array-decode": "Array of 32 Structs", + "default-make": "Construct 32 defaults", + "struct-decode": "Struct decode", + "struct-invalid": "Struct invalid decode", + "struct-is": "Struct guard", + "transform-decode": "32 transformations", + "union-decode": "Discriminated Union (8)" +} + +const table = ( + implementations: ReadonlyArray, + value: (implementation: string, caseName: string) => number, + unit: string, + digits: number +) => { + const labels: Record = { + "effect-aot": "Effect AOT", + "effect-interpreted": "Effect interpreted", + "effect-jit": "Effect JIT", + "valibot": "Valibot", + "zod-compiled": "Zod compile", + "zod-jitless": "Zod jitless" + } + const lines = [ + `| Case | ${implementations.map((implementation) => labels[implementation]).join(" | ")} |`, + `|---|${implementations.map(() => "---:").join("|")}|` + ] + for (const caseName of report.measurement.cases) { + lines.push( + `| ${names[caseName]} | ${ + implementations.map((implementation) => `${value(implementation, caseName).toFixed(digits)} ${unit}`).join(" | ") + } |` + ) + } + return lines.join("\n") +} + +const interpreted = ["effect-interpreted", "valibot", "zod-jitless"] +const compiled = ["effect-jit", "effect-aot", "zod-compiled"] +const environment = report.environment + +process.stdout.write(`# Schema compiler resource comparison + +Incremental costs are median per-schema slopes between ${lowCount} and ${highCount} distinct schemas across ${ + report.measurement.rounds +} fresh-process rounds. Lower is better. Fixed module imports and fixture inputs are excluded. + +Environment: Node ${environment.node}, V8 ${environment.v8}, ${environment.cpu}, ${environment.platform} ${ + environment.arch +}; Valibot ${environment.valibot}, Zod ${environment.zod}. + +Importing the JIT compiler retains ${ + (fixedDelta("effect-jit", "effect-interpreted", (sample) => sample.memory.module.heapBytes) / 1024).toFixed(1) +} KiB of additional fixed JavaScript heap and ${ + (fixedDelta("effect-jit", "effect-interpreted", (sample) => + sample.memory.module.codeBytes + sample.memory.module.bytecodeBytes + sample.memory.module.externalSourceBytes) / 1024) + .toFixed(1) +} KiB of V8 code, bytecode and source in this source-tree setup. + +## Retained JavaScript heap + +### Interpreted + +${table(interpreted, heap, "KiB/schema", 2)} + +### Compiled + +${table(compiled, heap, "KiB/schema", 2)} + +## Retained V8 code, bytecode and source + +${table(compiled, code, "KiB/schema", 2)} + +## Runtime preparation CPU + +This includes adapter creation and the first call. AOT also includes importing and installing the generated module. AOT build-time generation is excluded. + +### Interpreted + +${table(interpreted, startupCpu, "µs/schema", 1)} + +### Compiled + +${table(compiled, startupCpu, "µs/schema", 1)} + +## Peak runtime RSS growth + +${table(compiled, peakRss, "KiB/schema", 1)} + +## AOT build cost + +| Case | CPU | Generated source | Peak RSS growth | +|---|---:|---:|---:| +${report.measurement.cases.map((caseName) => { + const cpu = totalSlope( + "effect-aot", + caseName, + (sample) => (sample.aotBuild?.schemaCpu.cpuMicros ?? 0) + (sample.aotBuild?.generateCpu.cpuMicros ?? 0) + ) + const source = totalSlope("effect-aot", caseName, (sample) => sample.aotBuild?.sourceBytes ?? 0) / 1024 + const rss = totalSlope("effect-aot", caseName, (sample) => sample.aotBuild?.maxRssBytes ?? 0) / 1024 + return `| ${names[caseName]} | ${cpu.toFixed(1)} µs/schema | ${source.toFixed(2)} KiB/schema | ${rss.toFixed(1)} KiB/schema |` +}).join("\n")} +`) diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/resources.mts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/resources.mts new file mode 100644 index 000000000..8de96f956 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/resources.mts @@ -0,0 +1,479 @@ +import assert from "node:assert/strict" +import { execFileSync } from "node:child_process" +import { mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { join } from "node:path" +import { fileURLToPath, pathToFileURL } from "node:url" +import * as v8 from "node:v8" + +const implementations = [ + "effect-interpreted", + "effect-jit", + "effect-aot", + "valibot", + "zod-jitless", + "zod-compiled" +] as const +type Implementation = typeof implementations[number] + +const cases = [ + "struct-decode", + "struct-invalid", + "struct-is", + "array-decode", + "union-decode", + "transform-decode", + "default-make" +] as const +type CaseName = typeof cases[number] + +type BuiltCase = { + readonly schema: unknown + input: unknown + expected: unknown + readonly invalid?: boolean + readonly ast?: unknown +} + +type Parser = (input: unknown) => unknown + +const [command, root, implementationArgument, caseArgument, countArgument, output] = process.argv.slice(2) +const includes = (values: ReadonlyArray, value: string | undefined): value is A => + value !== undefined && values.includes(value as A) + +if (command !== "measure" && command !== "generate") { + throw new Error("command must be measure or generate") +} +if (root === undefined) throw new Error("root is required") +if (!includes(implementations, implementationArgument)) { + throw new Error(`implementation must be one of: ${implementations.join(", ")}`) +} +if (!includes(cases, caseArgument)) { + throw new Error(`case must be one of: ${cases.join(", ")}`) +} +const implementation: Implementation = implementationArgument +const caseName: CaseName = caseArgument +const count = Number(countArgument) +if (!Number.isSafeInteger(count) || count <= 0) throw new Error("count must be a positive integer") + +const forceGc = () => { + assert.equal(typeof globalThis.gc, "function", "run this probe with --expose-gc") + for (let i = 0; i < 5; i++) globalThis.gc!() +} + +type CpuSample = { + readonly cpuMicros: number + readonly systemMicros: number + readonly userMicros: number + readonly wallNanos: number +} + +const measureCpu = async (f: () => A | Promise): Promise => { + const cpuStart = process.cpuUsage() + const wallStart = process.hrtime.bigint() + const value = await f() + const wallNanos = Number(process.hrtime.bigint() - wallStart) + const cpu = process.cpuUsage(cpuStart) + return [value, { + cpuMicros: cpu.user + cpu.system, + systemMicros: cpu.system, + userMicros: cpu.user, + wallNanos + }] +} + +type MemorySample = { + readonly bytecodeBytes: number + readonly codeBytes: number + readonly externalSourceBytes: number + readonly heapBytes: number + readonly maxRssBytes: number + readonly rssBytes: number +} + +const memory = (): MemorySample => { + const usage = process.memoryUsage() + const code = v8.getHeapCodeStatistics() + return { + bytecodeBytes: code.bytecode_and_metadata_size, + codeBytes: code.code_and_metadata_size, + externalSourceBytes: code.external_script_source_size, + heapBytes: usage.heapUsed, + maxRssBytes: process.resourceUsage().maxRSS * 1024, + rssBytes: usage.rss + } +} + +const delta = (after: MemorySample, before: MemorySample) => ({ + bytecodeBytes: after.bytecodeBytes - before.bytecodeBytes, + codeBytes: after.codeBytes - before.codeBytes, + externalSourceBytes: after.externalSourceBytes - before.externalSourceBytes, + heapBytes: after.heapBytes - before.heapBytes, + maxRssBytes: after.maxRssBytes - before.maxRssBytes, + rssBytes: after.rssBytes - before.rssBytes +}) + +const perSchema = (sample: ReturnType) => ({ + bytecodeBytes: sample.bytecodeBytes / count, + codeBytes: sample.codeBytes / count, + externalSourceBytes: sample.externalSourceBytes / count, + heapBytes: sample.heapBytes / count, + maxRssBytes: sample.maxRssBytes / count, + rssBytes: sample.rssBytes / count +}) + +const loadEffectModule = (path: string) => + import(pathToFileURL(join(root, "packages/effect/src", `${path}.ts`)).href) + +const loadEffectSchemaModules = async () => ({ + Effect: await loadEffectModule("Effect"), + Schema: await loadEffectModule("Schema"), + SchemaAST: await loadEffectModule("SchemaAST") +}) + +type EffectSchemaModules = Awaited> + +const buildEffectCase = ({ Effect, Schema, SchemaAST }: EffectSchemaModules, index: number): BuiltCase => { + const suffix = String(index) + const name = `name${suffix}` + const age = `age${suffix}` + const active = `active${suffix}` + const person = () => Schema.Struct({ [name]: Schema.String, [age]: Schema.Number, [active]: Schema.Boolean }) + const value = { [name]: "Ada", [age]: 37, [active]: true } + switch (caseName) { + case "struct-decode": { + const schema = person() + return { schema, ast: schema.ast, input: value, expected: value } + } + case "struct-invalid": { + const schema = person() + return { schema, ast: schema.ast, input: { ...value, [age]: "bad" }, expected: true, invalid: true } + } + case "struct-is": { + const schema = person() + return { schema, ast: SchemaAST.toType(schema.ast), input: value, expected: true } + } + case "array-decode": { + const schema = Schema.Array(person()) + const input = Array.from({ length: 32 }, () => ({ ...value })) + return { schema, ast: schema.ast, input, expected: input } + } + case "union-decode": { + const tag = `tag${suffix}` + const schema = Schema.Union( + Array.from({ length: 8 }, (_, member) => + Schema.Struct({ [tag]: Schema.Literal(member), [`value${suffix}`]: Schema.Number })) + ) + const input = { [tag]: 7, [`value${suffix}`]: 1 } + return { schema, ast: schema.ast, input, expected: input } + } + case "transform-decode": { + const schema = Schema.Struct( + Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, Schema.NumberFromString])) + ) + const input = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, String(field)])) + const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field])) + return { schema, ast: schema.ast, input, expected } + } + case "default-make": { + const schema = Schema.Struct( + Object.fromEntries( + Array.from( + { length: 32 }, + (_, field) => [ + `v${suffix}_${field}`, + Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(field))) + ] + ) + ) + ) + const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field])) + return { schema, ast: SchemaAST.toType(schema.ast), input: {}, expected } + } + } +} + +const loadEffect = async (jit: boolean) => { + const modules = await loadEffectSchemaModules() + const SchemaParser = await loadEffectModule("SchemaParser") + const enable = jit ? (await loadEffectModule("unstable/schema/SchemaJITCompiler")).enable : undefined + + const prepare = (built: ReadonlyArray): ReadonlyArray => { + return built.map(({ ast, schema }) => { + if (enable !== undefined) enable(ast) + if (caseName === "struct-is") return SchemaParser.is(schema) + if (caseName === "default-make") return SchemaParser.make(schema) + return SchemaParser.decodeUnknownSync(schema) + }) + } + + return { build: (index: number) => buildEffectCase(modules, index), prepare } +} + +const loadZod = async (compiled: boolean) => { + const z = await import("zod/v4") + + const build = (index: number): BuiltCase => { + const suffix = String(index) + const name = `name${suffix}` + const age = `age${suffix}` + const active = `active${suffix}` + const person = () => z.object({ [name]: z.string(), [age]: z.number(), [active]: z.boolean() }) + const value = { [name]: "Ada", [age]: 37, [active]: true } + switch (caseName) { + case "struct-decode": + return { schema: person(), input: value, expected: value } + case "struct-invalid": + return { schema: person(), input: { ...value, [age]: "bad" }, expected: true, invalid: true } + case "struct-is": + return { schema: person(), input: value, expected: true } + case "array-decode": { + const input = Array.from({ length: 32 }, () => ({ ...value })) + return { schema: z.array(person()), input, expected: input } + } + case "union-decode": { + const tag = `tag${suffix}` + const schema = z.union( + Array.from({ length: 8 }, (_, member) => + z.object({ [tag]: z.literal(member), [`value${suffix}`]: z.number() })) as [ + ReturnType, + ReturnType, + ...Array> + ] + ) + const input = { [tag]: 7, [`value${suffix}`]: 1 } + return { schema, input, expected: input } + } + case "transform-decode": { + const schema = z.object( + Object.fromEntries( + Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, z.string().transform(Number)]) + ) + ) + const input = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, String(field)])) + const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field])) + return { schema, input, expected } + } + case "default-make": { + const schema = z.object( + Object.fromEntries( + Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, z.number().default(field)]) + ) + ) + const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field])) + return { schema, input: {}, expected } + } + } + } + + const prepare = (built: ReadonlyArray): ReadonlyArray => { + return built.map(({ schema }) => { + const target = compiled ? z.compile(schema, { strict: true }) : schema + if (caseName === "struct-is") { + return compiled + ? (input: unknown) => z.validate(target, input) + : (input: unknown) => z.validate(target, input, { jitless: true }) + } + return compiled + ? (input: unknown) => target.parse(input) + : (input: unknown) => target.parse(input, { jitless: true }) + }) + } + + return { build, prepare } +} + +const loadValibot = async () => { + const v = await import("valibot") + + const build = (index: number): BuiltCase => { + const suffix = String(index) + const name = `name${suffix}` + const age = `age${suffix}` + const active = `active${suffix}` + const person = () => v.object({ [name]: v.string(), [age]: v.number(), [active]: v.boolean() }) + const value = { [name]: "Ada", [age]: 37, [active]: true } + switch (caseName) { + case "struct-decode": + return { schema: person(), input: value, expected: value } + case "struct-invalid": + return { schema: person(), input: { ...value, [age]: "bad" }, expected: true, invalid: true } + case "struct-is": + return { schema: person(), input: value, expected: true } + case "array-decode": { + const input = Array.from({ length: 32 }, () => ({ ...value })) + return { schema: v.array(person()), input, expected: input } + } + case "union-decode": { + const tag = `tag${suffix}` + const schema = v.variant( + tag, + Array.from({ length: 8 }, (_, member) => + v.object({ [tag]: v.literal(member), [`value${suffix}`]: v.number() })) + ) + const input = { [tag]: 7, [`value${suffix}`]: 1 } + return { schema, input, expected: input } + } + case "transform-decode": { + const schema = v.object( + Object.fromEntries( + Array.from({ length: 32 }, (_, field) => [ + `v${suffix}_${field}`, + v.pipe(v.string(), v.transform(Number)) + ]) + ) + ) + const input = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, String(field)])) + const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field])) + return { schema, input, expected } + } + case "default-make": { + const schema = v.object( + Object.fromEntries( + Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, v.optional(v.number(), field)]) + ) + ) + const expected = Object.fromEntries(Array.from({ length: 32 }, (_, field) => [`v${suffix}_${field}`, field])) + return { schema, input: {}, expected } + } + } + } + + const prepare = (built: ReadonlyArray): ReadonlyArray => { + return built.map(({ schema }) => + caseName === "struct-is" + ? (input: unknown) => v.is(schema, input) + : (input: unknown) => v.parse(schema, input)) + } + + return { build, prepare } +} + +const runParser = (parser: Parser, built: BuiltCase): unknown => { + if (!built.invalid) return parser(built.input) + try { + parser(built.input) + return false + } catch { + return true + } +} + +const validate = (actual: unknown, built: BuiltCase) => assert.deepEqual(actual, built.expected) + +if (command === "generate") { + if (output === undefined) throw new Error("output is required for generate") + const [modules, moduleCpu] = await measureCpu(async () => { + return { + ...await loadEffectSchemaModules(), + AOT: await loadEffectModule("unstable/schema/SchemaAOTCompiler") + } + }) + const [built, schemaCpu] = await measureCpu(() => + Array.from({ length: count }, (_, index) => buildEffectCase(modules, index).ast) + ) + const operation = caseName === "struct-is" ? "is" : caseName === "default-make" ? "make" : "decode" + const [source, generateCpu] = await measureCpu(() => + modules.AOT.compile(built.map((ast) => ({ ast, operations: [operation] })))) + writeFileSync(output, source) + process.stdout.write(JSON.stringify({ + generateCpu, + heapBytes: process.memoryUsage().heapUsed, + maxRssBytes: process.resourceUsage().maxRSS * 1024, + moduleCpu, + schemaCpu, + sourceBytes: Buffer.byteLength(source) + })) +} else { + forceGc() + const beforeModule = memory() + const [library, moduleCpu] = await measureCpu(() => { + switch (implementation) { + case "effect-interpreted": + return loadEffect(false) + case "effect-jit": + case "effect-aot": + return loadEffect(implementation === "effect-jit") + case "zod-jitless": + case "zod-compiled": + return loadZod(implementation === "zod-compiled") + case "valibot": + return loadValibot() + } + }) + forceGc() + const afterModule = memory() + + const [built, schemaCpu] = await measureCpu(() => Array.from({ length: count }, (_, index) => library.build(index))) + forceGc() + const afterSchemas = memory() + + let aotBuild: unknown + let aotModuleCpu: CpuSample | undefined + let directory: string | undefined + let parsers: ReadonlyArray + let prepareCpu: CpuSample + try { + if (implementation === "effect-aot") { + directory = mkdtempSync(join(root, "packages/effect/.compiler-resources-")) + const generated = join(directory, "generated.mjs") + aotBuild = JSON.parse(execFileSync( + process.execPath, + ["--expose-gc", fileURLToPath(import.meta.url), "generate", root, implementation, caseName, String(count), generated], + { encoding: "utf8" } + )) + const [installed, measuredModuleCpu] = await measureCpu(async () => { + const generatedModule = await import(pathToFileURL(generated).href) + generatedModule.install(built.map((value) => value.ast)) + }) + void installed + aotModuleCpu = measuredModuleCpu + } + const prepared = await measureCpu(() => library.prepare(built)) + parsers = prepared[0] + prepareCpu = prepared[1] + forceGc() + const afterPrepare = memory() + + const [results, firstCallCpu] = await measureCpu(() => parsers.map((parser, index) => runParser(parser, built[index]))) + results.forEach((actual, index) => validate(actual, built[index])) + results.length = 0 + forceGc() + const afterFirstCall = memory() + + for (const value of built) { + value.input = undefined + value.expected = undefined + } + forceGc() + const afterRelease = memory() + + assert.equal(built.length, count) + assert.equal(parsers.length, count) + process.stdout.write(JSON.stringify({ + aotBuild, + case: caseName, + count, + cpu: { + aotModule: aotModuleCpu, + firstCall: firstCallCpu, + module: moduleCpu, + prepare: prepareCpu, + schema: schemaCpu + }, + implementation, + memory: { + compilerPerSchema: perSchema(delta(afterFirstCall, afterSchemas)), + firstCallPerSchema: perSchema(delta(afterFirstCall, afterPrepare)), + module: delta(afterModule, beforeModule), + preparePerSchema: perSchema(delta(afterPrepare, afterSchemas)), + retainedLibraryPerSchema: perSchema(delta(afterRelease, afterModule)), + schemaPerSchema: perSchema(delta(afterSchemas, afterModule)), + totalWithFixturePerSchema: perSchema(delta(afterFirstCall, afterModule)) + } + })) + } finally { + if (directory !== undefined) { + rmSync(directory, { recursive: true, force: true }) + } + } +} diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts new file mode 100644 index 000000000..daa0ccb47 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts @@ -0,0 +1,93 @@ +import { execFileSync } from "node:child_process" +import { mkdirSync, readFileSync, writeFileSync } from "node:fs" +import os from "node:os" +import { dirname, join, resolve } from "node:path" +import { createRequire } from "node:module" +import { fileURLToPath } from "node:url" + +const implementations = [ + "effect-interpreted", + "valibot", + "zod-jitless", + "effect-jit", + "effect-aot", + "zod-compiled" +] as const + +const cases = [ + "struct-decode", + "struct-invalid", + "struct-is", + "array-decode", + "union-decode", + "transform-decode", + "default-make" +] as const + +const [outputArgument = "tmp/schema-compiler-resources.json", roundsArgument = "5", countsArgument = "100,500"] = + process.argv.slice(2) +const rounds = Number(roundsArgument) +const counts = countsArgument.split(",").map(Number) +if (!Number.isSafeInteger(rounds) || rounds <= 0) throw new Error("rounds must be a positive integer") +if (counts.some((count) => !Number.isSafeInteger(count) || count <= 0)) { + throw new Error("counts must be comma-separated positive integers") +} + +const root = process.cwd() +const output = resolve(outputArgument) +const worker = fileURLToPath(new URL("./resources.mts", import.meta.url)) +const require = createRequire(import.meta.url) +const packageVersion = (name: string) => { + let directory = dirname(require.resolve(name)) + while (true) { + try { + const metadata = JSON.parse(readFileSync(join(directory, "package.json"), "utf8")) + if (metadata.name === name) return metadata.version as string + } catch { + // Continue at the parent directory. + } + const parent = dirname(directory) + if (parent === directory) throw new Error(`package.json not found for ${name}`) + directory = parent + } +} + +const results: Array = [] +for (let round = 0; round < rounds; round++) { + const orderedImplementations = round % 2 === 0 ? implementations : implementations.toReversed() + const orderedCounts = round % 2 === 0 ? counts : counts.toReversed() + for (const count of orderedCounts) { + for (const caseName of cases) { + for (const implementation of orderedImplementations) { + const sample = JSON.parse(execFileSync( + process.execPath, + ["--expose-gc", worker, "measure", root, implementation, caseName, String(count)], + { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 } + )) + results.push({ round, ...sample }) + } + } + process.stderr.write(`resource round ${round + 1}/${rounds}, count ${count} complete\n`) + } +} + +mkdirSync(dirname(output), { recursive: true }) +writeFileSync(output, JSON.stringify({ + environment: { + arch: process.arch, + cpu: os.cpus()[0]?.model ?? "unknown", + node: process.version, + platform: process.platform, + valibot: packageVersion("valibot"), + v8: process.versions.v8, + zod: packageVersion("zod") + }, + measurement: { + cases, + counts, + implementations, + rounds + }, + results +}, null, 2)) +process.stdout.write(`${output}\n`) diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/aot.ts b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/aot.ts new file mode 100644 index 000000000..1a4021125 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/aot.ts @@ -0,0 +1,17 @@ +import { execFileSync } from "node:child_process" +import { mkdtempSync, rmSync } from "node:fs" +import { join } from "node:path" +import { fileURLToPath, pathToFileURL } from "node:url" +import { roots } from "./cases.ts" + +const directory = mkdtempSync(fileURLToPath(new URL("./.aot-", import.meta.url))) +try { + const file = join(directory, "generated.mjs") + execFileSync(process.execPath, [fileURLToPath(new URL("./generate.mts", import.meta.url)), file]) + const generated = await import(pathToFileURL(file).href) + generated.install(roots) +} finally { + rmSync(directory, { recursive: true, force: true }) +} + +export { isExtraValid, isInvalid, isValid, parseExtraValid, parseInvalid, parseValid } from "./cases.ts" diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/cases.ts b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/cases.ts new file mode 100644 index 000000000..ba38d5ea0 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/cases.ts @@ -0,0 +1,65 @@ +import * as Schema from "effect/Schema" +import * as SchemaAST from "effect/SchemaAST" +import * as SchemaParser from "effect/SchemaParser" +import assert from "node:assert/strict" +import { invalidData, validData, validDataWithExtras } from "./data.ts" + +export const schema = Schema.Struct({ + number: Schema.Number, + negNumber: Schema.Number, + maxNumber: Schema.Number, + string: Schema.String, + longString: Schema.String, + boolean: Schema.Boolean, + deeplyNested: Schema.Struct({ + foo: Schema.String, + num: Schema.Number, + bool: Schema.Boolean + }) +}) + +export const targets = [ + { ast: schema.ast, operations: ["decode"] }, + { ast: SchemaAST.toType(schema.ast), operations: ["is"] }, + { ast: SchemaAST.flip(schema.ast), operations: ["decode"] } +] as const +export const roots = targets.map((target) => target.ast) + +const parseCase = (input: unknown, invalid = false) => () => { + const parse = SchemaParser.decodeUnknownSync(schema) + return { + run: invalid + ? () => { + try { + parse(input) + return false + } catch { + return true + } + } + : () => parse(input), + validate: invalid + ? (result: unknown) => assert.equal(result, true) + : (result: unknown) => assert.deepEqual(result, validData) + } +} + +const guardCase = (input: unknown, expected: boolean) => () => { + const guard = SchemaParser.is(schema) + return { + run: expected + ? () => { + if (!guard(input)) throw new Error("Invalid") + return true + } + : () => guard(input), + validate: (result: unknown) => assert.equal(result, expected) + } +} + +export const parseValid = parseCase(validData) +export const parseExtraValid = parseCase(validDataWithExtras) +export const parseInvalid = parseCase(invalidData, true) +export const isValid = guardCase(validData, true) +export const isExtraValid = guardCase(validDataWithExtras, true) +export const isInvalid = guardCase(invalidData, false) diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/data.ts b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/data.ts new file mode 100644 index 000000000..d247f4eb0 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/data.ts @@ -0,0 +1,31 @@ +// Extracted from moltar/typescript-runtime-type-benchmarks at +// d1791e68fc1108ef47da50547e80900e177a9d10. +// Upstream license: MIT, declared in package.json at that commit. +export const validData = Object.freeze({ + number: 1, + negNumber: -1, + maxNumber: Number.MAX_VALUE, + string: "string", + longString: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Vivendum intellegat et qui, ei denique consequuntur vix. Semper aeterno percipit ut his, sea ex utinam referrentur repudiandae. No epicuri hendrerit consetetur sit, sit dicta adipiscing ex, in facete detracto deterruisset duo. Quot populo ad qui. Sit fugit nostrum et. Ad per diam dicant interesset, lorem iusto sensibus ut sed. No dicam aperiam vis. Pri posse graeco definitiones cu, id eam populo quaestio adipiscing, usu quod malorum te. Ex nam agam veri, dicunt efficiantur ad qui, ad legere adversarium sit. Commune platonem mel id, brute adipiscing duo an. Vivendum intellegat et qui, ei denique consequuntur vix. Offendit eleifend moderatius ex vix, quem odio mazim et qui, purto expetendis cotidieque quo cu, veri persius vituperata ei nec. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + boolean: true, + deeplyNested: { + foo: "bar", + num: 1, + bool: false + } +}) + +export const validDataWithExtras = Object.freeze({ + ...validData, + extraAttribute: "foo", + deeplyNested: { + ...validData.deeplyNested, + extraNestedAttribute: "bar" + } +}) + +export const invalidData = Object.freeze({ + ...validData, + number: "invalid" +}) diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/generate.mts b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/generate.mts new file mode 100644 index 000000000..defdd093e --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/generate.mts @@ -0,0 +1,5 @@ +import { writeFileSync } from "node:fs" +import { compile } from "effect/unstable/schema/SchemaAOTCompiler" +import { targets } from "./cases.ts" + +writeFileSync(process.argv[2], compile(targets)) diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/interpreted.ts b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/interpreted.ts new file mode 100644 index 000000000..448732fda --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/interpreted.ts @@ -0,0 +1 @@ +export { isExtraValid, isInvalid, isValid, parseExtraValid, parseInvalid, parseValid } from "./cases.ts" diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/jit.ts b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/jit.ts new file mode 100644 index 000000000..005d5588a --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/jit.ts @@ -0,0 +1,6 @@ +import { enable } from "effect/unstable/schema/SchemaJITCompiler" +import { roots } from "./cases.ts" + +for (const ast of roots) enable(ast) + +export { isExtraValid, isInvalid, isValid, parseExtraValid, parseInvalid, parseValid } from "./cases.ts" diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/valibot.ts b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/valibot.ts new file mode 100644 index 000000000..4de6e0cb2 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/valibot.ts @@ -0,0 +1,53 @@ +import assert from "node:assert/strict" +import * as v from "valibot" +import { invalidData, validData, validDataWithExtras } from "./data.ts" + +const shape = { + number: v.number(), + negNumber: v.number(), + maxNumber: v.number(), + string: v.string(), + longString: v.string(), + boolean: v.boolean(), + deeplyNested: v.object({ + foo: v.string(), + num: v.number(), + bool: v.boolean() + }) +} + +const parseSchema = v.object(shape) +const guardSchema = v.looseObject({ ...shape, deeplyNested: v.looseObject(shape.deeplyNested.entries) }) + +const parseCase = (input: unknown, invalid = false) => () => ({ + run: invalid + ? () => { + try { + v.parse(parseSchema, input) + return false + } catch { + return true + } + } + : () => v.parse(parseSchema, input), + validate: invalid + ? (result: unknown) => assert.equal(result, true) + : (result: unknown) => assert.deepEqual(result, validData) +}) + +const guardCase = (input: unknown, expected: boolean) => () => ({ + run: expected + ? () => { + if (!v.is(guardSchema, input)) throw new Error("Invalid") + return true + } + : () => v.is(guardSchema, input), + validate: (result: unknown) => assert.equal(result, expected) +}) + +export const parseValid = parseCase(validData) +export const parseExtraValid = parseCase(validDataWithExtras) +export const parseInvalid = parseCase(invalidData, true) +export const isValid = guardCase(validData, true) +export const isExtraValid = guardCase(validDataWithExtras, true) +export const isInvalid = guardCase(invalidData, false) diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/zod.ts b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/zod.ts new file mode 100644 index 000000000..245da3a85 --- /dev/null +++ b/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/zod.ts @@ -0,0 +1,117 @@ +import assert from "node:assert/strict" +import * as z from "zod/v4" +import { invalidData, validData, validDataWithExtras } from "./data.ts" + +const makeShape = () => ({ + number: z.number(), + negNumber: z.number(), + maxNumber: z.number(), + string: z.string(), + longString: z.string(), + boolean: z.boolean(), + deeplyNested: z.object({ + foo: z.string(), + num: z.number(), + bool: z.boolean() + }) +}) + +const makeParseSchema = () => z.object(makeShape()) +const makeGuardSchema = () => { + const shape = makeShape() + return z.object({ ...shape, deeplyNested: shape.deeplyNested.passthrough() }).passthrough() +} + +const parseCase = ( + compile: (schema: ReturnType) => (input: unknown) => unknown, + input: unknown, + invalid = false +) => +() => { + const parse = compile(makeParseSchema()) + return { + run: invalid + ? () => { + try { + parse(input) + return false + } catch { + return true + } + } + : () => parse(input), + validate: invalid + ? (result: unknown) => assert.equal(result, true) + : (result: unknown) => assert.deepEqual(result, validData) + } +} + +const guardCase = ( + compile: (schema: ReturnType) => (input: unknown) => boolean, + input: unknown, + expected: boolean +) => +() => { + const validate = compile(makeGuardSchema()) + return { + run: expected + ? () => { + if (!validate(input)) throw new Error("Invalid") + return true + } + : () => validate(input), + validate: (result: unknown) => assert.equal(result, expected) + } +} + +const assertParseCase = ( + compile: (schema: ReturnType) => (input: unknown) => unknown, + input: unknown +) => +() => { + const parse = compile(makeGuardSchema()) + return { + run: () => { + parse(input) + return true + }, + validate: (result: unknown) => assert.equal(result, true) + } +} + +const parse = (schema: ReturnType) => (input: unknown) => schema.parse(input) +const parseJitless = (schema: ReturnType) => (input: unknown) => + schema.parse(input, { jitless: true }) +const parseCompiled = (schema: ReturnType) => { + const compiled = z.compile(schema, { strict: true }) + return (input: unknown) => compiled.parse(input) +} +const validate = (schema: ReturnType) => (input: unknown) => z.validate(schema, input) +const validateJitless = (schema: ReturnType) => (input: unknown) => + z.validate(schema, input, { jitless: true }) +const validateCompiled = (schema: ReturnType) => { + const compiled = z.compile(schema, { strict: true }) + return (input: unknown) => z.validate(compiled, input) +} +const assertParse = (schema: ReturnType) => (input: unknown) => schema.parse(input) + +export const parseValid = parseCase(parse, validData) +export const parseExtraValid = parseCase(parse, validDataWithExtras) +export const parseInvalid = parseCase(parse, invalidData, true) +export const parseJitlessValid = parseCase(parseJitless, validData) +export const parseJitlessExtraValid = parseCase(parseJitless, validDataWithExtras) +export const parseJitlessInvalid = parseCase(parseJitless, invalidData, true) +export const parseCompiledValid = parseCase(parseCompiled, validData) +export const parseCompiledExtraValid = parseCase(parseCompiled, validDataWithExtras) +export const parseCompiledInvalid = parseCase(parseCompiled, invalidData, true) +export const isValid = guardCase(validate, validData, true) +export const isExtraValid = guardCase(validate, validDataWithExtras, true) +export const isInvalid = guardCase(validate, invalidData, false) +export const isJitlessValid = guardCase(validateJitless, validData, true) +export const isJitlessExtraValid = guardCase(validateJitless, validDataWithExtras, true) +export const isJitlessInvalid = guardCase(validateJitless, invalidData, false) +export const isCompiledValid = guardCase(validateCompiled, validData, true) +export const isCompiledExtraValid = guardCase(validateCompiled, validDataWithExtras, true) +export const isCompiledInvalid = guardCase(validateCompiled, invalidData, false) +export const assertParseValid = assertParseCase(assertParse, validData) +export const assertParseExtraValid = assertParseCase(assertParse, validDataWithExtras) diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts b/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts index b21c0dd2d..06ddb89a5 100644 --- a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts +++ b/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts @@ -66,7 +66,10 @@ const parsingCase = (input, errors, success) => () => { const run = Schema.decodeUnknownOption(makeSchema()) return { run: () => run(input, { errors }), - validate: (result) => assert.equal(Option.isSome(result), success) + validate: (result) => { + assert.equal(Option.isSome(result), success) + if (success && Option.isSome(result)) assert.deepEqual(result.value, validData) + } } } diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts b/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts index fdff6a1b5..25676aad2 100644 --- a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts +++ b/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts @@ -16,7 +16,7 @@ const makeSchema = () => { }) const rating = v.object({ id: v.number(), - stars: v.pipe(v.number(), v.minValue(1), v.maxValue(5)), + stars: v.pipe(v.number(), v.minValue(0), 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) @@ -56,7 +56,10 @@ const parsingCase = (input, options, success) => () => { const schema = makeSchema() return { run: () => v.safeParse(schema, input, options), - validate: (result) => assert.equal(result.success, success) + validate: (result) => { + assert.equal(result.success, success) + if (success) assert.deepEqual(result.output, validData) + } } } @@ -72,6 +75,7 @@ const standardCase = (input, success) => () => { validate: (result) => { assert.equal(typeof result?.then, "undefined") assert.equal(result.issues === undefined, success) + if (success) assert.deepEqual(result.value, validData) } } } diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts b/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts index 81e56f482..609f06c49 100644 --- a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts +++ b/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts @@ -29,7 +29,7 @@ const makeSchema = () => { 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), + quantity: z.number().min(1).max(10), tags: z.array(z.string().min(1).max(30)), images: z.array(image), ratings: z.array(rating) @@ -46,7 +46,10 @@ const parsingCase = (input, success) => () => { const options = { jitless: true } return { run: () => schema.safeParse(input, options), - validate: (result) => assert.equal(result.success, success) + validate: (result) => { + assert.equal(result.success, success) + if (success) assert.deepEqual(result.data, validData) + } } } @@ -60,6 +63,7 @@ const standardCase = (input, success) => () => { validate: (result) => { assert.equal(typeof result?.then, "undefined") assert.equal(result.issues === undefined, success) + if (success) assert.deepEqual(result.value, validData) } } } diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts b/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts index 2a84cab07..f9fd2ac05 100644 --- a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts +++ b/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts @@ -11,6 +11,11 @@ const schema = Schema.Struct({ const input = { a: "a", b: 1 } const invalidInput = { a: "a", b: "invalid" } +export const makeValid = () => ({ + run: () => schema.make(input), + validate: (result) => assert.deepEqual(result, input) +}) + export const parserExitInvalid = () => { const run = SchemaParser.decodeUnknownExit(schema) return { @@ -23,7 +28,10 @@ export const exitValid = () => { const run = Schema.decodeUnknownExit(schema) return { run: () => run(input), - validate: (result) => assert.equal(result._tag, "Success") + validate: (result) => { + assert.equal(result._tag, "Success") + assert.deepEqual(result.value, input) + } } } @@ -39,7 +47,10 @@ export const optionValid = () => { const run = Schema.decodeUnknownOption(schema) return { run: () => run(input), - validate: (result) => assert.equal(Option.isSome(result), true) + validate: (result) => { + assert.equal(Option.isSome(result), true) + if (Option.isSome(result)) assert.deepEqual(result.value, input) + } } } @@ -55,7 +66,10 @@ export const resultValid = () => { const run = Schema.decodeUnknownResult(schema) return { run: () => run(input), - validate: (result) => assert.equal(Result.isSuccess(result), true) + validate: (result) => { + assert.equal(Result.isSuccess(result), true) + if (Result.isSuccess(result)) assert.deepEqual(result.success, input) + } } } diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts b/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts index 8ed02deaf..dc7895913 100644 --- a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts +++ b/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts @@ -7,25 +7,37 @@ import assert from "node:assert/strict" const decodeCase = (schema, input, success, options) => () => { const run = Schema.decodeUnknownExit(schema, options) + const isOutput = success ? Schema.is(schema) : undefined return { run: () => run(input), - validate: (result) => assert.equal(result._tag, success ? "Success" : "Failure") + validate: (result) => { + assert.equal(result._tag, success ? "Success" : "Failure") + if (result._tag === "Success") assert.equal(isOutput?.(result.value), true) + } } } const decodeParserCase = (schema, input, success, options) => () => { const run = SchemaParser.decodeUnknownExit(schema, options) + const isOutput = success ? Schema.is(schema) : undefined return { run: () => run(input), - validate: (result) => assert.equal(result._tag, success ? "Success" : "Failure") + validate: (result) => { + assert.equal(result._tag, success ? "Success" : "Failure") + if (result._tag === "Success") assert.equal(isOutput?.(result.value), true) + } } } const encodeParserCase = (schema, input, success, options) => () => { const run = SchemaParser.encodeUnknownExit(schema, options) + const isOutput = success ? Schema.is(Schema.flip(schema)) : undefined return { run: () => run(input), - validate: (result) => assert.equal(result._tag, success ? "Success" : "Failure") + validate: (result) => { + assert.equal(result._tag, success ? "Success" : "Failure") + if (result._tag === "Success") assert.equal(isOutput?.(result.value), true) + } } } @@ -146,7 +158,7 @@ export const optionalPresentValid = decodeCase( 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))), + decode: SchemaGetter.transformOptionalEffect((input) => Effect.suspend(() => Effect.succeed(input))), encode: SchemaGetter.passthrough() })) const suspendedObjectFields = Object.fromEntries( @@ -162,6 +174,32 @@ export const object32SuspendedMiddleValid = decodeParserCase( true ) +const suspendedArrayInput = Array.from({ length: 32 }, (_, index) => `value${index}`) +const suspendedObjectAllFields = Object.fromEntries( + Array.from({ length: 32 }, (_, index) => [`field${index}`, suspendedString]) +) + +export const array32SuspendedConcurrent4 = decodeParserCase( + Schema.Array(suspendedString), + suspendedArrayInput, + true, + { concurrency: 4 } +) + +export const object32SuspendedConcurrent4 = decodeParserCase( + Schema.Struct(suspendedObjectAllFields), + suspendedObjectInput, + true, + { concurrency: 4 } +) + +export const record32SuspendedConcurrent4 = decodeParserCase( + Schema.Record(Schema.String, suspendedString), + suspendedObjectInput, + true, + { concurrency: 4 } +) + const literal2 = Schema.Literals(["value0", "value1"]) const literal100 = Schema.Literals(Array.from({ length: 100 }, (_, index) => `value${index}`)) const homogeneousUnion100 = Schema.Union( @@ -189,19 +227,6 @@ export const taggedWithFallbackValid = decodeParserCase( 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)) diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts b/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts index 2f62d5ac0..7ad53b379 100644 --- a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts +++ b/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts @@ -14,7 +14,32 @@ const makeEffectCheckedSchema = () => const makeEffectTemplateLiteralSchema = () => Schema.TemplateLiteral(["prefix-", Schema.String, "-middle-", Schema.Number, "-suffix"]) +const templateLiteral256Parts = Array.from({ length: 256 }, (_, index) => Schema.Literal(`part${index}`)) +const makeEffectTemplateLiteral256Schema = () => Schema.TemplateLiteral(templateLiteral256Parts) + const makeEffectRecordSchema = () => Schema.Record(Schema.String, Schema.String) +const makeEffectEncodedRecordSchema = () => + Schema.Record( + Schema.String.pipe(Schema.decodeTo(Schema.Number, SchemaTransformation.passthrough())), + Schema.String + ) + +const makeEffectObject2Schema = () => + Schema.Struct({ + name: Schema.String, + age: Schema.Number + }) + +const makeObjectFields = (size: number) => + Object.fromEntries(Array.from({ length: size }, (_, index) => [`field${index}`, Schema.NonEmptyString])) + +const object32Fields = makeObjectFields(32) +const object256Fields = makeObjectFields(256) + +const makeEffectObject32Schema = () => Schema.Struct(object32Fields) +const makeEffectObject256Schema = () => Schema.Struct(object256Fields) + +const object2Input = { name: "John", age: 42 } const literalValues100 = Array.from({ length: 100 }, (_, index) => `value${index}`) @@ -53,9 +78,42 @@ export const effectSchemaCreationTemplateLiteral = () => ({ validate: (schema) => assert.equal(schema.ast._tag, "TemplateLiteral") }) +export const effectSchemaCreationTemplateLiteral256 = () => ({ + run: makeEffectTemplateLiteral256Schema, + validate: (schema) => assert.equal(schema.ast._tag, "TemplateLiteral") +}) + +export const effectSchemaCreationObject2 = () => ({ + run: makeEffectObject2Schema, + validate: (schema) => assert.equal(schema.ast._tag, "Objects") +}) + +export const effectSchemaCreationObject32 = () => ({ + run: makeEffectObject32Schema, + validate: (schema) => assert.equal(schema.ast._tag, "Objects") +}) + +export const effectSchemaCreationObject256 = () => ({ + run: makeEffectObject256Schema, + validate: (schema) => assert.equal(schema.ast._tag, "Objects") +}) + +export const effectSchemaCreationEncodedRecord = () => ({ + run: makeEffectEncodedRecordSchema, + validate: (schema) => assert.equal(schema.ast._tag, "Objects") +}) + +export const effectFirstMakeObject2 = () => ({ + run: () => makeEffectObject2Schema().make(object2Input), + validate: (result) => assert.deepEqual(result, object2Input) +}) + export const effectFirstDecodeCheckedObject32 = () => ({ run: () => SchemaParser.decodeUnknownExit(makeEffectCheckedSchema())(input), - validate: (result) => assert.equal(result._tag, "Success") + validate: (result) => { + assert.equal(result._tag, "Success") + assert.deepEqual(result.value, input) + } }) export const effectFirstDecodeTemplateLiteral = () => ({ @@ -65,17 +123,26 @@ export const effectFirstDecodeTemplateLiteral = () => ({ export const effectFirstDecodeRecord32 = () => ({ run: () => SchemaParser.decodeUnknownExit(makeEffectRecordSchema())(input), - validate: (result) => assert.equal(result._tag, "Success") + validate: (result) => { + assert.equal(result._tag, "Success") + assert.deepEqual(result.value, input) + } }) export const effectFirstDecodeLiteral100 = () => ({ run: () => SchemaParser.decodeUnknownExit(makeEffectLiteral100Schema())("value99"), - validate: (result) => assert.equal(result._tag, "Success") + validate: (result) => { + assert.equal(result._tag, "Success") + assert.equal(result.value, "value99") + } }) export const effectFirstDecodeTagged100 = () => ({ run: () => SchemaParser.decodeUnknownExit(makeEffectTagged100Schema())(taggedInput), - validate: (result) => assert.equal(result._tag, "Success") + validate: (result) => { + assert.equal(result._tag, "Success") + assert.deepEqual(result.value, taggedInput) + } }) export const effectFirstDecodeEncodingChain8 = () => ({ diff --git a/repos/effect/packages/effect/runtimeperf/test/registry.test.mts b/repos/effect/packages/effect/runtimeperf/test/registry.test.mts index 1598b6344..4596e8bc9 100644 --- a/repos/effect/packages/effect/runtimeperf/test/registry.test.mts +++ b/repos/effect/packages/effect/runtimeperf/test/registry.test.mts @@ -2,14 +2,33 @@ 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" +import { loadRegistry, scenarioBatchSize } from "../utils.mts" describe("runtimeperf registry", () => { + it("uses the Effect calibration for every implementation in a scenario", () => { + const zod = { implementation: "zod4-compiled" } + const effect = { implementation: "effect" } + assert.equal(scenarioBatchSize([zod, effect], new Map([ + [zod, { batchSize: 4_096 }], + [effect, { batchSize: 256 }] + ])), 256) + }) + 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", "fast-check-v4", "valibot", "zod4"].includes(fixture.implementation)) + assert.ok([ + "effect", + "effect-aot", + "effect-jit", + "fast-check-v4", + "valibot", + "zod4", + "zod4-compiled", + "zod4-jitless", + "zod4-validate" + ].includes(fixture.implementation)) } }) @@ -19,7 +38,7 @@ describe("runtimeperf registry", () => { fixtures.filter((fixture) => fixture.suite === "arbitrary"), (fixture) => fixture.scenario ) - assert.equal(scenarios.size, 31) + assert.equal(scenarios.size, 33) for (const fixtures of scenarios.values()) { assert.deepEqual(fixtures.map((fixture) => fixture.implementation).sort(), ["effect", "fast-check-v4"]) const metadata = (fixture) => ({ @@ -103,7 +122,7 @@ describe("runtimeperf registry", () => { const { fixtures } = loadRegistry() const zodFiles = new Set( fixtures - .filter((fixture) => fixture.implementation === "zod4") + .filter((fixture) => fixture.suite === "schema-benchmarks" && fixture.implementation === "zod4") .map((fixture) => fixture.fixturePath) ) assert.ok(zodFiles.size > 0) @@ -115,6 +134,34 @@ describe("runtimeperf registry", () => { } }) + it("uses strict Zod compilation for the compiler comparison fixtures", async () => { + const { fixtures } = loadRegistry() + const compiled = fixtures.filter((fixture) => + fixture.suite === "compiler-rebuild" && fixture.implementation === "zod4-compiled" + ) + assert.equal(compiled.length, 19) + const paths = new Set(compiled.map((fixture) => fixture.fixturePath)) + assert.equal(paths.size, 1) + const source = await readFile([...paths][0], "utf8") + assert.match(source, /from "\.\/zod-cases\.ts"/) + const shared = await readFile(new URL("../suites/compiler-rebuild/fixtures/zod-cases.ts", import.meta.url), "utf8") + assert.match(shared, /from "zod\/v4"/) + assert.match(shared, /z\.compile\(value\.schema, \{ strict: true \}\)/) + }) + + it("uses interpreted Zod for the jitless compiler comparison fixtures", async () => { + const { fixtures } = loadRegistry() + const jitless = fixtures.filter((fixture) => + fixture.suite === "compiler-rebuild" && fixture.implementation === "zod4-jitless" + ) + assert.equal(jitless.length, 11) + const paths = new Set(jitless.map((fixture) => fixture.fixturePath)) + assert.equal(paths.size, 1) + const source = await readFile(new URL("../suites/compiler-rebuild/fixtures/zod-cases.ts", import.meta.url), "utf8") + assert.match(source, /z\.validate\(value\.schema, input, \{ jitless: true \}\)/) + assert.match(source, /value\.schema\.parse\(input, \{ jitless: true \}\)/) + }) + it("loads, runs and validates every fixture export", async () => { const { fixtures } = loadRegistry() const modules = new Map() diff --git a/repos/effect/packages/effect/runtimeperf/utils.mts b/repos/effect/packages/effect/runtimeperf/utils.mts index 2dc6210b0..2df3859b5 100644 --- a/repos/effect/packages/effect/runtimeperf/utils.mts +++ b/repos/effect/packages/effect/runtimeperf/utils.mts @@ -8,6 +8,11 @@ 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 runPath = join(runtimeperfDir, "run.mts") +export const comparePath = join(runtimeperfDir, "compare.mts") +export const materializePath = join(runtimeperfDir, "materialize.mts") +export const statsPath = join(runtimeperfDir, "stats.mts") +export const utilsPath = fileURLToPath(import.meta.url) export const workerPath = join(runtimeperfDir, "worker.mts") export const configPath = join(runtimeperfDir, "config.json") export const resultsRoot = join(repoRoot, "tmp", "runtimeperf", "results") @@ -147,7 +152,7 @@ export const selectFixtures = (fixtures, options, { effectOnly = false } = {}) = selected = selected.filter((fixture) => fixture.implementation === options.implementation) } if (effectOnly) { - selected = selected.filter((fixture) => fixture.implementation === "effect") + selected = selected.filter((fixture) => fixture.implementation.startsWith("effect")) } if (selected.length === 0) { throw new Error("No runtimeperf fixtures matched the selection") @@ -216,6 +221,13 @@ export const measureFixture = (fixture, defaults, batchSize, fixturePath = fixtu export const aggregateMeasurements = (measurements) => aggregate(measurements.map((item) => item.nsPerOp)) +export const scenarioBatchSize = (fixtures, calibrations) => { + const reference = fixtures.find((fixture) => fixture.implementation === "effect") ?? fixtures[0] + const calibration = calibrations.get(reference) + if (calibration === undefined) throw new Error("Missing scenario reference calibration") + return calibration.batchSize +} + export const coverageSummary = (fixtures) => ({ tiers: [...new Set(fixtures.map((fixture) => fixture.tier))].sort(), families: [...new Set(fixtures.map((fixture) => fixture.family))].sort(), @@ -223,7 +235,7 @@ export const coverageSummary = (fixtures) => ({ effectAstTags: [ ...new Set( fixtures - .filter((fixture) => fixture.implementation === "effect") + .filter((fixture) => fixture.implementation.startsWith("effect")) .flatMap((fixture) => fixture.astTags) ) ].sort() diff --git a/repos/effect/packages/effect/src/Array.ts b/repos/effect/packages/effect/src/Array.ts index 032af6080..8813ded8c 100644 --- a/repos/effect/packages/effect/src/Array.ts +++ b/repos/effect/packages/effect/src/Array.ts @@ -909,6 +909,18 @@ export const isReadonlyArrayNonEmpty: (self: ReadonlyArray) => self is Non */ export const length = (self: ReadonlyArray): number => self.length +/** + * Checks whether a string represents a JavaScript array index: a non-negative + * integer below `2 ** 32 - 1`, written without leading zeroes, a sign, or + * exponent notation. + * + * @internal + */ +export function isCanonicalArrayIndex(key: string): boolean { + const index = Number(key) + return String(index) === key && Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 +} + /** @internal */ export function isOutOfBounds(i: number, as: ReadonlyArray): boolean { return !Number.isFinite(i) || i < 0 || i >= as.length diff --git a/repos/effect/packages/effect/src/ByteSize.ts b/repos/effect/packages/effect/src/ByteSize.ts index 7dbba16b5..ddacee0f7 100644 --- a/repos/effect/packages/effect/src/ByteSize.ts +++ b/repos/effect/packages/effect/src/ByteSize.ts @@ -5,19 +5,14 @@ * * @since 4.0.0 */ -import * as Combiner from "./Combiner.ts" -import * as Equal from "./Equal.ts" +import * as BI from "./BigInt.ts" +import type * as Brand from "./Brand.ts" +import type * as Combiner from "./Combiner.ts" import type * as Equ from "./Equivalence.ts" import { dual } from "./Function.ts" -import * as Hash from "./Hash.ts" -import type * as Inspectable from "./Inspectable.ts" -import { NodeInspectSymbol } from "./Inspectable.ts" import * as Option from "./Option.ts" -import * as order from "./Order.ts" -import type { Pipeable } from "./Pipeable.ts" -import { pipeArguments } from "./Pipeable.ts" -import { hasProperty } from "./Predicate.ts" -import * as Reducer from "./Reducer.ts" +import type * as order from "./Order.ts" +import type * as Reducer from "./Reducer.ts" const TypeId = "~effect/ByteSize" @@ -32,18 +27,50 @@ const binaryBase = BigInt(1024) * @category models * @since 4.0.0 */ -export interface ByteSize extends Equal.Equal, Pipeable, Inspectable.Inspectable { - readonly [TypeId]: typeof TypeId - readonly value: bigint -} +export type ByteSize = Brand.Branded /** * Values accepted by byte-size decoding operations. * + * **Details** + * + * String inputs require a non-negative decimal integer without leading zeros, + * followed by a canonical unit symbol or lowercase unit name, optionally + * separated by one space. Numeric inputs are validated at runtime. + * + * @see {@link fromString} for external strings and fractional quantities + * @see {@link fromStringUnsafe} for throwing string validation * @category models * @since 4.0.0 */ -export type Input = ByteSize | bigint | number | string +export type Input = ByteSize | bigint | number | `${IntegerQuantity}${"" | " "}${InputUnit}` + +// Combining these constraints excludes signs, radix prefixes, leading zeros, +// fractions, and exponent notation without limiting the number of digits. +type IntegerQuantity = "0" | (`${bigint}` & `${"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"}${string}`) + +type InputUnit = + | Unit + | `${ + | "" + | "kilo" + | "mega" + | "giga" + | "tera" + | "peta" + | "exa" + | "zetta" + | "yotta" + | "ronna" + | "quetta" + | "kibi" + | "mebi" + | "gibi" + | "tebi" + | "pebi" + | "exbi" + | "zebi" + | "yobi"}byte${"" | "s"}` /** * Canonical decimal byte unit symbols. @@ -147,33 +174,7 @@ const allUnits = [...decimalUnits, ...binaryUnits.slice(1)] const unitsByName = new Map(allUnits.flatMap((unit) => unit.names.map((name) => [name, unit] as const))) const unitsBySymbol = new Map(allUnits.map((unit) => [unit.symbol, unit] as const)) -const ByteSizeProto: Omit = { - [TypeId]: TypeId, - [Hash.symbol](this: ByteSize) { - return Hash.hash(this.value) - }, - [Equal.symbol](this: ByteSize, that: unknown): boolean { - return isByteSize(that) && this.value === that.value - }, - toString(this: ByteSize) { - return `${this.value} ${this.value === bigint1 ? "byte" : "bytes"}` - }, - toJSON(this: ByteSize) { - return { _id: "ByteSize", bytes: `${this.value}` } - }, - [NodeInspectSymbol]() { - return this.toJSON() - }, - pipe() { - return pipeArguments(this, arguments) - } -} - -const make = (value: bigint): ByteSize => { - const byteSize = Object.create(ByteSizeProto) - byteSize.value = value - return byteSize -} +const make = (value: bigint): ByteSize => value as ByteSize /** * The byte size containing zero bytes. @@ -206,7 +207,21 @@ const fromQuantity = (quantity: number | bigint, unit: UnitInfo): ByteSize => { return make(BigInt(value)) } -const parse = (input: string): ByteSize => { +/** + * Parses a byte-size string and throws for invalid syntax, unsupported units, + * or quantities that do not represent non-negative integral bytes. + * + * **Details** + * + * Accepts decimal fractions, canonical SI and IEC symbols, lowercase unit + * names, and whitespace around the quantity and unit. Parsing uses exact + * bigint arithmetic, including above the safe integer range. + * + * @see {@link fromString} for non-throwing validation + * @category constructors + * @since 4.0.0 + */ +export const fromStringUnsafe = (input: string): ByteSize => { const match = /^\s*(\d+)(?:\.(\d+))?\s*([A-Za-z]+)\s*$/.exec(input) if (match === null) return invalid(`unsupported syntax ${JSON.stringify(input)}`) const unit = unitsByName.get(match[3]) @@ -220,6 +235,21 @@ const parse = (input: string): ByteSize => { return make(numerator / scale) } +/** + * Parses a byte-size string, returning `None` for invalid syntax, unsupported + * units, or quantities that do not represent non-negative integral bytes. + * + * **Details** + * + * Accepts external strings and fractional quantities using the same syntax + * as `fromStringUnsafe`. + * + * @see {@link fromStringUnsafe} for throwing validation + * @category constructors + * @since 4.0.0 + */ +export const fromString: (input: string) => Option.Option = Option.liftThrowable(fromStringUnsafe) + /** * Decodes a trusted input into a byte size and throws for invalid input. * @@ -234,9 +264,7 @@ export const fromInputUnsafe = (input: Input): ByteSize => { case "number": return fromNumber(input) case "string": - return parse(input) - case "object": - if (isByteSize(input)) return input + return fromStringUnsafe(input) } return invalid(`unsupported input ${input}`) } @@ -267,126 +295,126 @@ const unitConstructor = (symbol: Unit) => (value: number | bigint): ByteSize => * @category constructors * @since 4.0.0 */ -export const kilobytes = unitConstructor("kB") +export const kilobytes: (value: number | bigint) => ByteSize = unitConstructor("kB") /** * Creates a decimal megabyte value. * * @category constructors * @since 4.0.0 */ -export const megabytes = unitConstructor("MB") +export const megabytes: (value: number | bigint) => ByteSize = unitConstructor("MB") /** * Creates a decimal gigabyte value. * * @category constructors * @since 4.0.0 */ -export const gigabytes = unitConstructor("GB") +export const gigabytes: (value: number | bigint) => ByteSize = unitConstructor("GB") /** * Creates a decimal terabyte value. * * @category constructors * @since 4.0.0 */ -export const terabytes = unitConstructor("TB") +export const terabytes: (value: number | bigint) => ByteSize = unitConstructor("TB") /** * Creates a decimal petabyte value. * * @category constructors * @since 4.0.0 */ -export const petabytes = unitConstructor("PB") +export const petabytes: (value: number | bigint) => ByteSize = unitConstructor("PB") /** * Creates a decimal exabyte value. * * @category constructors * @since 4.0.0 */ -export const exabytes = unitConstructor("EB") +export const exabytes: (value: number | bigint) => ByteSize = unitConstructor("EB") /** * Creates a decimal zettabyte value. * * @category constructors * @since 4.0.0 */ -export const zettabytes = unitConstructor("ZB") +export const zettabytes: (value: number | bigint) => ByteSize = unitConstructor("ZB") /** * Creates a decimal yottabyte value. * * @category constructors * @since 4.0.0 */ -export const yottabytes = unitConstructor("YB") +export const yottabytes: (value: number | bigint) => ByteSize = unitConstructor("YB") /** * Creates a decimal ronnabyte value. * * @category constructors * @since 4.0.0 */ -export const ronnabytes = unitConstructor("RB") +export const ronnabytes: (value: number | bigint) => ByteSize = unitConstructor("RB") /** * Creates a decimal quettabyte value. * * @category constructors * @since 4.0.0 */ -export const quettabytes = unitConstructor("QB") +export const quettabytes: (value: number | bigint) => ByteSize = unitConstructor("QB") /** * Creates a binary kibibyte value. * * @category constructors * @since 4.0.0 */ -export const kibibytes = unitConstructor("KiB") +export const kibibytes: (value: number | bigint) => ByteSize = unitConstructor("KiB") /** * Creates a binary mebibyte value. * * @category constructors * @since 4.0.0 */ -export const mebibytes = unitConstructor("MiB") +export const mebibytes: (value: number | bigint) => ByteSize = unitConstructor("MiB") /** * Creates a binary gibibyte value. * * @category constructors * @since 4.0.0 */ -export const gibibytes = unitConstructor("GiB") +export const gibibytes: (value: number | bigint) => ByteSize = unitConstructor("GiB") /** * Creates a binary tebibyte value. * * @category constructors * @since 4.0.0 */ -export const tebibytes = unitConstructor("TiB") +export const tebibytes: (value: number | bigint) => ByteSize = unitConstructor("TiB") /** * Creates a binary pebibyte value. * * @category constructors * @since 4.0.0 */ -export const pebibytes = unitConstructor("PiB") +export const pebibytes: (value: number | bigint) => ByteSize = unitConstructor("PiB") /** * Creates a binary exbibyte value. * * @category constructors * @since 4.0.0 */ -export const exbibytes = unitConstructor("EiB") +export const exbibytes: (value: number | bigint) => ByteSize = unitConstructor("EiB") /** * Creates a binary zebibyte value. * * @category constructors * @since 4.0.0 */ -export const zebibytes = unitConstructor("ZiB") +export const zebibytes: (value: number | bigint) => ByteSize = unitConstructor("ZiB") /** * Creates a binary yobibyte value. * * @category constructors * @since 4.0.0 */ -export const yobibytes = unitConstructor("YiB") +export const yobibytes: (value: number | bigint) => ByteSize = unitConstructor("YiB") /** * Checks whether a value is a byte size. @@ -394,7 +422,7 @@ export const yobibytes = unitConstructor("YiB") * @category guards * @since 4.0.0 */ -export const isByteSize = (input: unknown): input is ByteSize => hasProperty(input, TypeId) +export const isByteSize = (input: unknown): input is ByteSize => typeof input === "bigint" && input >= bigint0 /** * Checks whether a byte size is zero. @@ -402,7 +430,7 @@ export const isByteSize = (input: unknown): input is ByteSize => hasProperty(inp * @category predicates * @since 4.0.0 */ -export const isZero = (self: ByteSize): boolean => self.value === bigint0 +export const isZero = (self: ByteSize): boolean => self === bigint0 /** * Returns the exact byte count as a bigint. @@ -410,7 +438,7 @@ export const isZero = (self: ByteSize): boolean => self.value === bigint0 * @category getters * @since 4.0.0 */ -export const toBigInt = (self: ByteSize): bigint => self.value +export const toBigInt = (self: ByteSize): bigint => self /** * Converts a byte size to a safe integer, returning `None` when it is too large. @@ -418,10 +446,7 @@ export const toBigInt = (self: ByteSize): bigint => self.value * @category converting * @since 4.0.0 */ -export const toNumber = (self: ByteSize): Option.Option => { - const value = Number(self.value) - return Number.isSafeInteger(value) ? Option.some(value) : Option.none() -} +export const toNumber: (self: ByteSize) => Option.Option = BI.toNumber /** * Converts a byte size to a safe integer and throws when it is too large. @@ -430,7 +455,7 @@ export const toNumber = (self: ByteSize): Option.Option => { * @since 4.0.0 */ export const toNumberUnsafe = (self: ByteSize): number => - Option.getOrThrowWith(toNumber(self), () => new Error(`ByteSize exceeds Number.MAX_SAFE_INTEGER: ${self.value}`)) + Option.getOrThrowWith(toNumber(self), () => new Error(`ByteSize exceeds Number.MAX_SAFE_INTEGER: ${self}`)) /** * Converts a byte size to an approximate number of the specified unit. @@ -441,7 +466,7 @@ export const toNumberUnsafe = (self: ByteSize): number => export const toUnit: { (unit: Unit): (self: ByteSize) => number (self: ByteSize, unit: Unit): number -} = dual(2, (self: ByteSize, unit: Unit) => Number(self.value) / Number(unitsBySymbol.get(unit)!.factor)) +} = dual(2, (self: ByteSize, unit: Unit) => Number(self) / Number(unitsBySymbol.get(unit)!.factor)) /** * Provides an order for byte sizes. @@ -449,9 +474,7 @@ export const toUnit: { * @category instances * @since 4.0.0 */ -export const Order: order.Order = order.make((self, that) => - self.value < that.value ? -1 : self.value > that.value ? 1 : 0 -) +export const Order: order.Order = BI.Order /** * Provides an equivalence for byte sizes. @@ -459,7 +482,7 @@ export const Order: order.Order = order.make((self, that) => * @category instances * @since 4.0.0 */ -export const Equivalence: Equ.Equivalence = (self, that) => self.value === that.value +export const Equivalence: Equ.Equivalence = BI.Equivalence /** * Returns whether a byte size is in an inclusive range. @@ -470,7 +493,7 @@ export const Equivalence: Equ.Equivalence = (self, that) => self.value export const between: { (options: { minimum: ByteSize; maximum: ByteSize }): (self: ByteSize) => boolean (self: ByteSize, options: { minimum: ByteSize; maximum: ByteSize }): boolean -} = order.isBetween(Order) +} = BI.between /** * Returns the smaller byte size. @@ -481,7 +504,7 @@ export const between: { export const min: { (that: ByteSize): (self: ByteSize) => ByteSize (self: ByteSize, that: ByteSize): ByteSize -} = order.min(Order) +} = BI.min as any /** * Returns the larger byte size. @@ -492,7 +515,7 @@ export const min: { export const max: { (that: ByteSize): (self: ByteSize) => ByteSize (self: ByteSize, that: ByteSize): ByteSize -} = order.max(Order) +} = BI.max as any /** * Constrains a byte size to an inclusive range. @@ -503,7 +526,7 @@ export const max: { export const clamp: { (options: { minimum: ByteSize; maximum: ByteSize }): (self: ByteSize) => ByteSize (self: ByteSize, options: { minimum: ByteSize; maximum: ByteSize }): ByteSize -} = order.clamp(Order) +} = BI.clamp as any /** * Checks whether the first byte size is less than the second. @@ -514,7 +537,7 @@ export const clamp: { export const isLessThan: { (that: ByteSize): (self: ByteSize) => boolean (self: ByteSize, that: ByteSize): boolean -} = order.isLessThan(Order) +} = BI.isLessThan /** * Checks whether the first byte size is at most the second. @@ -525,7 +548,7 @@ export const isLessThan: { export const isLessThanOrEqualTo: { (that: ByteSize): (self: ByteSize) => boolean (self: ByteSize, that: ByteSize): boolean -} = order.isLessThanOrEqualTo(Order) +} = BI.isLessThanOrEqualTo /** * Checks whether the first byte size is greater than the second. @@ -536,7 +559,7 @@ export const isLessThanOrEqualTo: { export const isGreaterThan: { (that: ByteSize): (self: ByteSize) => boolean (self: ByteSize, that: ByteSize): boolean -} = order.isGreaterThan(Order) +} = BI.isGreaterThan /** * Checks whether the first byte size is at least the second. @@ -547,7 +570,7 @@ export const isGreaterThan: { export const isGreaterThanOrEqualTo: { (that: ByteSize): (self: ByteSize) => boolean (self: ByteSize, that: ByteSize): boolean -} = order.isGreaterThanOrEqualTo(Order) +} = BI.isGreaterThanOrEqualTo /** * Checks whether two byte sizes contain the same count. @@ -569,7 +592,7 @@ export const equals: { export const sum: { (that: ByteSize): (self: ByteSize) => ByteSize (self: ByteSize, that: ByteSize): ByteSize -} = dual(2, (self: ByteSize, that: ByteSize) => make(self.value + that.value)) +} = BI.sum as any /** * Subtracts byte sizes, returning `None` on underflow. @@ -582,8 +605,7 @@ export const subtract: { (self: ByteSize, that: ByteSize): Option.Option } = dual( 2, - (self: ByteSize, that: ByteSize) => - self.value < that.value ? Option.none() : Option.some(make(self.value - that.value)) + (self: ByteSize, that: ByteSize) => self < that ? Option.none() : Option.some(make(self - that)) ) /** @@ -596,8 +618,8 @@ export const subtractUnsafe: { (that: ByteSize): (self: ByteSize) => ByteSize (self: ByteSize, that: ByteSize): ByteSize } = dual(2, (self: ByteSize, that: ByteSize) => { - if (self.value < that.value) throw new Error(`ByteSize subtraction underflow: ${self.value} - ${that.value}`) - return make(self.value - that.value) + if (self < that) throw new Error(`ByteSize subtraction underflow: ${self} - ${that}`) + return make(self - that) }) const scalar = (input: number | bigint, positive: boolean): bigint | undefined => { @@ -616,7 +638,7 @@ export const times: { (self: ByteSize, multiplier: number | bigint): Option.Option } = dual(2, (self: ByteSize, multiplier: number | bigint) => { const value = scalar(multiplier, false) - return value === undefined ? Option.none() : Option.some(make(self.value * value)) + return value === undefined ? Option.none() : Option.some(make(self * value)) }) /** @@ -630,7 +652,7 @@ export const divide: { (self: ByteSize, divisor: number | bigint): Option.Option } = dual(2, (self: ByteSize, divisor: number | bigint) => { const value = scalar(divisor, true) - return value === undefined ? Option.none() : Option.some(make(self.value / value)) + return value === undefined ? Option.none() : Option.some(make(self / value)) }) const validatePrecision = (precision: number): number => { @@ -660,19 +682,19 @@ export const format = (self: ByteSize, options: FormatOptions = {}): string => { const precision = validatePrecision(options.precision ?? 2) const trailingZeros = options.trailingZeros ?? false if (options.unit !== undefined) { - return formatWithUnit(self.value, unitsBySymbol.get(options.unit)!, precision, trailingZeros) + return formatWithUnit(self, unitsBySymbol.get(options.unit)!, precision, trailingZeros) } const units = options.system === "decimal" ? decimalUnits : binaryUnits - if (self.value === bigint0) return "0 B" + if (self === bigint0) return "0 B" let index = units.length - 1 - while (index > 0 && self.value < units[index].factor) index-- + while (index > 0 && self < units[index].factor) index-- if (index < units.length - 1) { const scale = BigInt(10) ** BigInt(precision) - const rounded = (self.value * scale * BigInt(2) + units[index].factor) / (units[index].factor * BigInt(2)) + const rounded = (self * scale * BigInt(2) + units[index].factor) / (units[index].factor * BigInt(2)) const base = options.system === "decimal" ? decimalBase : binaryBase if (rounded >= base * scale) index++ } - return formatWithUnit(self.value, units[index], precision, trailingZeros) + return formatWithUnit(self, units[index], precision, trailingZeros) } /** @@ -681,7 +703,7 @@ export const format = (self: ByteSize, options: FormatOptions = {}): string => { * @category math * @since 4.0.0 */ -export const ReducerSum: Reducer.Reducer = Reducer.make(sum, zero) +export const ReducerSum: Reducer.Reducer = BI.ReducerSum as any /** * Combiner that keeps the largest byte size. @@ -689,7 +711,7 @@ export const ReducerSum: Reducer.Reducer = Reducer.make(sum, zero) * @category math * @since 4.0.0 */ -export const CombinerMax: Combiner.Combiner = Combiner.max(Order) +export const CombinerMax: Combiner.Combiner = BI.CombinerMax as any /** * Combiner that keeps the smallest byte size. @@ -697,4 +719,4 @@ export const CombinerMax: Combiner.Combiner = Combiner.max(Order) * @category math * @since 4.0.0 */ -export const CombinerMin: Combiner.Combiner = Combiner.min(Order) +export const CombinerMin: Combiner.Combiner = BI.CombinerMin as any diff --git a/repos/effect/packages/effect/src/Cache.ts b/repos/effect/packages/effect/src/Cache.ts index d1e3091b2..0bc335802 100644 --- a/repos/effect/packages/effect/src/Cache.ts +++ b/repos/effect/packages/effect/src/Cache.ts @@ -447,7 +447,10 @@ export const get: { MutableHashMap.remove(self.map, key) } }) - MutableHashMap.set(self.map, key, entry) + const exit = entry.fiber.pollUnsafe() + if (exit === undefined || !effect.exitHasInterrupts(exit)) { + MutableHashMap.set(self.map, key, entry) + } if (Number.isFinite(self.capacity)) { checkCapacity(self) } @@ -1041,6 +1044,10 @@ export const invalidateWhen: { return oentry.await().pipe( effect.map((value) => { if (f(value)) { + const current = MutableHashMap.get(self.map, key) + if (Option.isNone(current) || current.value !== oentry) { + return false + } MutableHashMap.remove(self.map, key) return true } @@ -1165,12 +1172,18 @@ export const refresh: { } entry.fiber.addObserver((exit) => { if (effect.exitHasInterrupts(exit)) { - if (!existing) 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) if (Duration.isZero(ttl)) { - MutableHashMap.remove(self.map, key) + const current = MutableHashMap.get(self.map, key) + if (existing || (Option.isSome(current) && current.value === entry)) { + MutableHashMap.remove(self.map, key) + } return effect.void } entry.expiresAt = Duration.isFinite(ttl) @@ -1178,6 +1191,7 @@ export const refresh: { : undefined if (existing) { MutableHashMap.set(self.map, key, entry) + checkCapacity(self) } }) return entry.await() diff --git a/repos/effect/packages/effect/src/Cause.ts b/repos/effect/packages/effect/src/Cause.ts index 2511f27bc..9cda574ab 100644 --- a/repos/effect/packages/effect/src/Cause.ts +++ b/repos/effect/packages/effect/src/Cause.ts @@ -261,7 +261,7 @@ export declare namespace Cause { readonly [ReasonTypeId]: typeof ReasonTypeId readonly _tag: Tag readonly annotations: ReadonlyMap - annotate(annotations: Context.Context | ReadonlyMap, options?: { + annotate(annotations: Context.Context, options?: { readonly overwrite?: boolean | undefined }): this } diff --git a/repos/effect/packages/effect/src/Channel.ts b/repos/effect/packages/effect/src/Channel.ts index d119dcf6c..5845d91ba 100644 --- a/repos/effect/packages/effect/src/Channel.ts +++ b/repos/effect/packages/effect/src/Channel.ts @@ -2035,7 +2035,7 @@ export const mapDoneEffect: { ) const concurrencyIsSequential = ( - concurrency: number | "unbounded" | undefined + concurrency: Types.Concurrency | undefined ) => concurrency === undefined || (concurrency !== "unbounded" && concurrency <= 1) /** @@ -2074,7 +2074,7 @@ export const mapEffect: { ( f: (d: OutElem, i: number) => Effect.Effect, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly unordered?: boolean | undefined } ): ( @@ -2084,7 +2084,7 @@ export const mapEffect: { self: Channel, f: (d: OutElem, i: number) => Effect.Effect, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly unordered?: boolean | undefined } ): Channel @@ -2094,7 +2094,7 @@ export const mapEffect: { self: Channel, f: (d: OutElem, i: number) => Effect.Effect, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly unordered?: boolean | undefined } ): Channel => @@ -2138,7 +2138,7 @@ const mapEffectConcurrent = < self: Channel, f: (o: OutElem, i: number) => Effect.Effect, options: { - readonly concurrency: number | "unbounded" + readonly concurrency: Types.Concurrency readonly unordered?: boolean | undefined } ): Channel => @@ -2320,7 +2320,7 @@ export const tap: { ( f: (d: Types.NoInfer) => Effect.Effect, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined } ): ( self: Channel @@ -2329,7 +2329,7 @@ export const tap: { self: Channel, f: (d: Types.NoInfer) => Effect.Effect, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined } ): Channel } = dual( @@ -2338,7 +2338,7 @@ export const tap: { self: Channel, f: (d: Types.NoInfer) => Effect.Effect, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined } ): Channel => mapEffect(self, (a) => Effect.as(f(a), a), options) @@ -2384,7 +2384,7 @@ export const flatMap: { ( f: (d: OutElem) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): ( @@ -2417,7 +2417,7 @@ export const flatMap: { self: Channel, f: (d: OutElem) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): Channel< @@ -2450,7 +2450,7 @@ export const flatMap: { self: Channel, f: (d: OutElem) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): Channel< @@ -2543,7 +2543,7 @@ const flatMapConcurrent = < self: Channel, f: (d: OutElem) => Channel, options: { - readonly concurrency: number | "unbounded" + readonly concurrency: Types.Concurrency readonly bufferSize?: number | undefined } ): Channel< @@ -4143,6 +4143,97 @@ export const catchCause: { }) })) +/** + * Recovers from defects using the provided function. + * + * **Details** + * + * Typed failures and interruptions are not caught. + * + * **Example** (Recovering from a defect) + * + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" + * + * const channel = Channel.fromEffect(Effect.die("boom")).pipe( + * Channel.catchDefect((defect) => Channel.succeed(`recovered: ${defect}`)) + * ) + * + * Effect.runSync(Channel.runCollect(channel)) // => ["recovered: boom"] + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchDefect: { + ( + f: (defect: unknown) => Channel + ): ( + self: Channel + ) => Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (defect: unknown) => Channel + ): Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + f: (defect: unknown) => Channel +): Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => catchCauseFilter(self, Cause.findDefect, f)) + /** * Runs an effect with the full failure `Cause` when the channel fails, then * fails the returned channel with the original cause. @@ -5983,7 +6074,7 @@ export const switchMap: { ( f: (d: OutElem) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): ( @@ -6016,7 +6107,7 @@ export const switchMap: { self: Channel, f: (d: OutElem) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): Channel< @@ -6049,7 +6140,7 @@ export const switchMap: { self: Channel, f: (d: OutElem) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): Channel< @@ -6109,7 +6200,7 @@ export const switchMap: { */ export const mergeAll: { (options: { - readonly concurrency: number | "unbounded" + readonly concurrency: Types.Concurrency readonly bufferSize?: number | undefined readonly switch?: boolean | undefined }): ( @@ -6142,7 +6233,7 @@ export const mergeAll: { Env >, options: { - readonly concurrency: number | "unbounded" + readonly concurrency: Types.Concurrency readonly bufferSize?: number | undefined readonly switch?: boolean | undefined } @@ -6168,7 +6259,7 @@ export const mergeAll: { Env >, { bufferSize = 16, concurrency, switch: switch_ = false }: { - readonly concurrency: number | "unbounded" + readonly concurrency: Types.Concurrency readonly bufferSize?: number | undefined readonly switch?: boolean | undefined } @@ -6515,9 +6606,8 @@ export const splitLines = (): Channel< // Accumulates text that has not yet been terminated by a line break. // Content is carried across chunks until a terminator is found. let stringBuilder = "" - // Set when a chunk ends with \r so the next chunk can check whether - // the following character is \n (completing a \r\n pair) or not - // (standalone \r, which is itself a line terminator). + // A trailing \r completes the line immediately. Remember it only to + // suppress a leading \n in the next nonempty string. let midCRLF = false // Remembers the upstream Done value after the first time the upstream // signals completion, so subsequent pulls return Done immediately @@ -6544,11 +6634,8 @@ export const splitLines = (): Channel< let indexOfLF = str.indexOf("\n") if (midCRLF) { if (indexOfLF === 0) { - pushLine("") from = 1 indexOfLF = str.indexOf("\n", from) - } else { - pushLine("") } midCRLF = false } @@ -6558,18 +6645,19 @@ export const splitLines = (): Channel< from = indexOfLF + 1 indexOfLF = str.indexOf("\n", from) } else { + pushLine(str.substring(from, indexOfCR)) if (str.length === indexOfCR + 1) { midCRLF = true + from = str.length indexOfCR = -1 } else { - pushLine(str.substring(from, indexOfCR)) from = indexOfCR + (indexOfLF === indexOfCR + 1 ? 2 : 1) indexOfCR = str.indexOf("\r", from) indexOfLF = str.indexOf("\n", from) } } } - stringBuilder = stringBuilder + str.substring(from, str.length - (midCRLF ? 1 : 0)) + stringBuilder = stringBuilder + str.substring(from) } } return Arr.isReadonlyArrayNonEmpty(chunkBuilder) ? chunkBuilder : null @@ -6584,7 +6672,7 @@ export const splitLines = (): Channel< onFailure: Effect.failCause, onDone: (leftover) => { done = Option.some(leftover) - if (stringBuilder.length > 0 || midCRLF) { + if (stringBuilder.length > 0) { const last = stringBuilder stringBuilder = "" midCRLF = false @@ -7699,7 +7787,7 @@ export const bind: { name: Exclude, f: (a: NoInfer) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): ( @@ -7734,7 +7822,7 @@ export const bind: { name: Exclude, f: (a: NoInfer) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): Channel< @@ -7767,7 +7855,7 @@ export const bind: { name: Exclude, f: (a: NoInfer) => Channel, options?: { - readonly concurrency?: number | "unbounded" | undefined + readonly concurrency?: Types.Concurrency | undefined readonly bufferSize?: number | undefined } ): Channel< @@ -8068,16 +8156,6 @@ export const runCollect = ( return acc }) -/** - * Runs a channel and outputs the done value. - * - * @category running - * @since 4.0.0 - */ -export const runDone = ( - self: Channel -): Effect.Effect => runWith(self, identity_, Effect.succeed) - /** * Runs a channel until the first output element is available, returning it in * an `Option`. diff --git a/repos/effect/packages/effect/src/ChannelSchema.ts b/repos/effect/packages/effect/src/ChannelSchema.ts index 6f91859ec..b2a84f438 100644 --- a/repos/effect/packages/effect/src/ChannelSchema.ts +++ b/repos/effect/packages/effect/src/ChannelSchema.ts @@ -12,6 +12,7 @@ import * as Channel from "./Channel.ts" import * as Effect from "./Effect.ts" import { dual } from "./Function.ts" import * as Schema from "./Schema.ts" +import type * as SchemaAST from "./SchemaAST.ts" /** * Creates a channel that encodes non-empty chunks of schema values into the @@ -96,7 +97,8 @@ export const encodeUnknown: ( * @since 4.0.0 */ export const decode = ( - schema: S + schema: S, + options?: SchemaAST.ParseOptions ) => (): Channel.Channel< Arr.NonEmptyReadonlyArray, @@ -107,7 +109,7 @@ export const decode = ( Done, S["DecodingServices"] > => { - const decode = Schema.decodeEffect(Schema.NonEmptyArray(schema)) + const decode = Schema.decodeEffect(Schema.NonEmptyArray(schema), options) return Channel.fromTransform((upstream, _scope) => Effect.succeed(Effect.flatMap(upstream, (chunk) => decode(chunk)))) } @@ -131,7 +133,8 @@ export const decode = ( * @since 4.0.0 */ export const decodeUnknown: ( - schema: S + schema: S, + options?: SchemaAST.ParseOptions ) => () => Channel.Channel< Arr.NonEmptyReadonlyArray, IE | Schema.SchemaError, diff --git a/repos/effect/packages/effect/src/Chunk.ts b/repos/effect/packages/effect/src/Chunk.ts index 1fe33b94d..1e6a6d94a 100644 --- a/repos/effect/packages/effect/src/Chunk.ts +++ b/repos/effect/packages/effect/src/Chunk.ts @@ -249,7 +249,7 @@ const makeChunk = (backing: Backing): Chunk => { } case "ISlice": { chunk.length = backing.length - chunk.depth = backing.chunk.depth + 1 + chunk.depth = 0 chunk.left = _empty chunk.right = _empty break diff --git a/repos/effect/packages/effect/src/Config.ts b/repos/effect/packages/effect/src/Config.ts index c6eadaba4..3db34fb22 100644 --- a/repos/effect/packages/effect/src/Config.ts +++ b/repos/effect/packages/effect/src/Config.ts @@ -12,7 +12,6 @@ import * as ConfigProvider from "./ConfigProvider.ts" import * as Effect from "./Effect.ts" import * as Effectable from "./Effectable.ts" import { dual, memoize } from "./Function.ts" -import * as InternalConfig from "./internal/config.ts" import * as InternalRecord from "./internal/record.ts" import * as LogLevel_ from "./LogLevel.ts" import * as Option from "./Option.ts" @@ -692,7 +691,7 @@ const decodeFromCursor = ( SchemaAST.unknown, ast, new SchemaTransformation.Transformation( - SchemaGetter.transformOrFail((input: unknown) => decode(input as ConfigCursor)), + SchemaGetter.transformEffect((input: unknown) => decode(input as ConfigCursor)), SchemaGetter.passthrough() ) ) @@ -1194,7 +1193,8 @@ export function Array>( const arrayString = Schema.String.pipe( Schema.decodeTo(Schema.toCodecStringTree(array), { decode: SchemaGetter.split(resolvedOptions), - encode: SchemaGetter.passthrough, Schema.StringTree>({ strict: false }).compose( + encode: SchemaGetter.compose( + SchemaGetter.passthrough, Schema.StringTree>({ strict: false }), SchemaGetter.transform((input) => input.join(separator)) ) }) @@ -1271,7 +1271,8 @@ export function Record< const recordString = Schema.String.pipe( Schema.decodeTo(Schema.toCodecStringTree(record), { decode: split.decode, - encode: SchemaGetter.passthrough, Schema.StringTree>({ strict: false }).compose( + encode: SchemaGetter.compose( + SchemaGetter.passthrough, Schema.StringTree>({ strict: false }), split.encode ) }) @@ -1314,7 +1315,7 @@ export function Record< * @since 2.0.0 */ export function Boolean(name?: string) { - return schema(InternalConfig.boolean, name) + return schema(Schema.BooleanLiterals, name) } /** diff --git a/repos/effect/packages/effect/src/ConfigProvider.ts b/repos/effect/packages/effect/src/ConfigProvider.ts index 9d588b77d..8dd6622c3 100644 --- a/repos/effect/packages/effect/src/ConfigProvider.ts +++ b/repos/effect/packages/effect/src/ConfigProvider.ts @@ -9,6 +9,7 @@ * @since 4.0.0 */ +import * as Arr from "./Array.ts" import * as Context from "./Context.ts" import * as Data from "./Data.ts" import * as Effect from "./Effect.ts" @@ -958,8 +959,6 @@ function buildEnvTrie(env: Record): EnvTrieNode { return trie } -const NUMERIC_INDEX = /^(0|[1-9][0-9]*)$/ - function nodeAtEnv( trie: EnvTrieNode, env: Record, @@ -976,7 +975,7 @@ function nodeAtEnv( return leafValue === undefined ? undefined : makeValue(leafValue) } - const allNumeric = children.every((k) => NUMERIC_INDEX.test(k)) + const allNumeric = children.every(Arr.isCanonicalArrayIndex) if (allNumeric) { const length = Math.max(...children.map((k) => parseInt(k, 10))) + 1 return makeArray(length, leafValue) @@ -1136,7 +1135,7 @@ function interpolate(envValue: string, parsed: Record): string { : defaultValue ?? "" return interpolate( - envValue.replace(group, value), + envValue.replace(group, () => value), parsed ) } diff --git a/repos/effect/packages/effect/src/Context.ts b/repos/effect/packages/effect/src/Context.ts index 3db89c975..52ee4d14c 100644 --- a/repos/effect/packages/effect/src/Context.ts +++ b/repos/effect/packages/effect/src/Context.ts @@ -1031,6 +1031,7 @@ export const getUnsafe: { * @since 2.0.0 */ export const get: { + (service: Key): (self: Context) => S (service: Key): (self: Context) => S (self: Context, service: Key): S } = getUnsafe diff --git a/repos/effect/packages/effect/src/Crypto.ts b/repos/effect/packages/effect/src/Crypto.ts index 489525f05..fa16326e0 100644 --- a/repos/effect/packages/effect/src/Crypto.ts +++ b/repos/effect/packages/effect/src/Crypto.ts @@ -11,6 +11,8 @@ */ import * as Context from "./Context.ts" import * as Effect from "./Effect.ts" +import * as random from "./internal/random.ts" +import * as Ulid from "./internal/ulid.ts" import * as Uuid from "./internal/uuid.ts" import * as PlatformError from "./PlatformError.ts" @@ -151,6 +153,21 @@ export interface Crypto { * Generates a cryptographically secure UUIDv7 string. */ readonly randomUUIDv7: Effect.Effect + + /** + * Generates a cryptographically secure ULID string. + * + * **Details** + * + * ULIDs contain 26 uppercase Crockford base32 characters. The first 10 encode + * the `Clock` timestamp in milliseconds; the remaining 16 encode 80 random + * bits. ULIDs sort by timestamp, with no ordering guarantee within the same + * millisecond. + * + * Timestamp normalization matches UUIDv7: fractions are truncated, values are + * clamped to the 48-bit range, and `NaN` encodes as zero. + */ + readonly randomULID: Effect.Effect } /** @@ -251,7 +268,7 @@ export const make = ( random: Effect.sync(() => nextDoubleUnsafe()), randomBoolean: Effect.sync(() => nextDoubleUnsafe() > 0.5), randomInt: Effect.sync(() => nextIntUnsafe()), - randomBetween: (min, max) => Effect.sync(() => nextDoubleUnsafe() * (max - min) + min), + randomBetween: (min, max) => Effect.sync(() => random.nextBetween(min, max, nextDoubleUnsafe())), randomIntBetween(min, max, options) { const extra = options?.halfOpen === true ? 0 : 1 return Effect.sync(() => { @@ -274,6 +291,9 @@ export const make = ( randomUUIDv4: Effect.sync(() => Uuid.v4String(randomBytesUnsafe(16))), randomUUIDv7: Effect.clockWith((clock) => Effect.succeed(Uuid.v7String(clock.currentTimeMillisUnsafe(), randomBytesUnsafe(16))) + ), + randomULID: Effect.clockWith((clock) => + Effect.succeed(Ulid.ulidString(clock.currentTimeMillisUnsafe(), randomBytesUnsafe(10))) ) }) } diff --git a/repos/effect/packages/effect/src/Deferred.ts b/repos/effect/packages/effect/src/Deferred.ts index afe2ab3a9..14e75b964 100644 --- a/repos/effect/packages/effect/src/Deferred.ts +++ b/repos/effect/packages/effect/src/Deferred.ts @@ -117,6 +117,12 @@ const DeferredProto = { } } +const DeferredImpl = function(this: any) { + this.resumes = undefined + this.effect = undefined +} as unknown as { new(): Deferred; prototype: any } +DeferredImpl.prototype = DeferredProto + /** * Creates an empty `Deferred` synchronously outside the `Effect` runtime. * @@ -137,12 +143,7 @@ const DeferredProto = { * @category unsafe * @since 4.0.0 */ -export const makeUnsafe = (): Deferred => { - const self = Object.create(DeferredProto) - self.resumes = undefined - self.effect = undefined - return self -} +export const makeUnsafe = (): Deferred => new DeferredImpl() /** * Creates a new `Deferred`. diff --git a/repos/effect/packages/effect/src/Effect.ts b/repos/effect/packages/effect/src/Effect.ts index 15c76d4aa..476db097f 100644 --- a/repos/effect/packages/effect/src/Effect.ts +++ b/repos/effect/packages/effect/src/Effect.ts @@ -40,7 +40,7 @@ import { CurrentLogAnnotations, CurrentLogSpans } from "./References.ts" import type * as Request from "./Request.ts" import type { RequestResolver } from "./RequestResolver.ts" import type * as Result from "./Result.ts" -import type { Schedule } from "./Schedule.ts" +import type { Metadata as ScheduleMetadata, Schedule } from "./Schedule.ts" import type { Scheduler } from "./Scheduler.ts" import type { Scope } from "./Scope.ts" import type { @@ -227,7 +227,7 @@ export type Services = T extends Effect ? _R * @category guards * @since 2.0.0 */ -export const isEffect: (u: unknown) => u is Effect = core.isEffect +export const isEffect: (u: unknown) => u is Effect = core.isEffect /** * Iterator interface for Effect generators, enabling Effect values to work with generator functions. @@ -256,6 +256,7 @@ export interface EffectIterator> { * Namespace containing type utilities for the `Effect.all` function, which handles * collecting multiple effects into various output structures. * + * @category collecting * @since 2.0.0 */ export declare namespace All { @@ -331,13 +332,8 @@ export declare namespace All { ] ? Mode extends true ? Result.Result<_A, _E> : _A : never }, - Mode extends true ? never - : keyof T extends never ? never - : T[keyof T] extends Effect ? _E - : never, - keyof T extends never ? never - : T[keyof T] extends Effect ? _R - : never + Mode extends true ? never : Error>, + Services> > : never @@ -377,6 +373,8 @@ export declare namespace All { : [Arg] extends [Iterable] ? ReturnIterable, IsResult> : [Arg] extends [Record] ? ReturnObject, IsResult> : never + + type ObjectValues = T extends unknown ? T[keyof T] : never } /** @@ -488,7 +486,7 @@ export declare namespace All { * ``` * * @see {@link forEach} for iterating over elements and applying an effect. - * @category combining + * @category collecting * @since 2.0.0 */ export const all: < @@ -529,7 +527,7 @@ export const all: < * await Effect.runPromise(program) // => [['0 is even', '2 is even'], [1, 3]] * ``` * - * @category filtering + * @category collecting * @since 2.0.0 */ export const partition: { @@ -578,7 +576,7 @@ export const partition: { * output // => ["Adding 1 at index 0", "Adding 2 at index 1", "Adding 3 at index 2", 6] * ``` * - * @category folding + * @category collecting * @since 2.0.0 */ export const reduce: { @@ -617,7 +615,7 @@ export const reduce: { * await Effect.runPromiseExit(program) // => Exit.fail(["0 is even", "2 is even"]) * ``` * - * @category validation + * @category collecting * @since 2.0.0 */ export const validate: { @@ -671,7 +669,7 @@ export const validate: { * await Effect.runPromise(program) // => Option.some(3) * ``` * - * @category searching + * @category collecting * @since 2.0.0 */ export const findFirst: { @@ -699,7 +697,7 @@ export const findFirst: { * * @see {@link findFirst} for the simpler effectful predicate-based variant * - * @category searching + * @category collecting * @since 4.0.0 */ export const findFirstFilter: { @@ -773,7 +771,7 @@ export const findFirstFilter: { * ``` * * @see {@link all} for combining multiple effects into one. - * @category sequencing + * @category collecting * @since 2.0.0 */ export const forEach: { @@ -811,7 +809,7 @@ export const forEach: { * await Effect.runPromise(empty) // => Option.none() * ``` * - * @category getters + * @category collecting * @since 2.0.0 */ export const head: ( @@ -966,11 +964,12 @@ export const promise: ( * @category constructors * @since 2.0.0 */ -export const tryPromise: ( - options: - | { readonly try: (signal: AbortSignal) => PromiseLike; readonly catch: (error: unknown) => E } - | ((signal: AbortSignal) => PromiseLike) -) => Effect = internal.tryPromise +export const tryPromise: { + (options: (signal: AbortSignal) => PromiseLike): Effect + ( + options: { readonly try: (signal: AbortSignal) => PromiseLike; readonly catch: (error: unknown) => E } + ): Effect +} = internal.tryPromise /** * Creates an `Effect` that always succeeds with a given value. @@ -1272,7 +1271,7 @@ export const never: Effect = internal.never * Effect.runSync(program) // => { x: 2, y: 3, sum: 5 } * ``` * - * @category constructors + * @category do notation * @since 2.0.0 */ export const Do: Effect<{}> = internal.Do @@ -1289,7 +1288,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 mapping + * @category do notation * @since 2.0.0 */ export const bindTo: { @@ -1331,7 +1330,7 @@ export { * @see {@link Do} for starting from an empty accumulated record * @see {@link gen} for sequencing without accumulating a record * - * @category mapping + * @category do notation * @since 2.0.0 */ let_ as let @@ -1361,7 +1360,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 sequencing + * @category do notation * @since 2.0.0 */ export const bind: { @@ -1461,6 +1460,7 @@ export const gen: { /** * Type helpers for `Effect.gen` generator return signatures. * + * @category constructors * @since 2.0.0 */ export declare namespace gen { @@ -1635,12 +1635,13 @@ export const failCauseSync: ( */ export const die: (defect: unknown) => Effect = internal.die -const try_: ( - options: { +const try_: { + (options: LazyArg): Effect + (options: { readonly try: LazyArg readonly catch: (error: unknown) => E - } | LazyArg -) => Effect = internal.try + }): Effect +} = internal.try export { /** @@ -1777,6 +1778,26 @@ export const withFiber: ( evaluate: (fiber: Fiber) => Effect ) => Effect = core.withFiber +/** + * Accesses the current fiber to compute a successful value. + * + * **Example** (Computing a value from the current fiber) + * + * ```ts import.meta.vitest + * import { Effect } from "effect" + * + * const program = Effect.withFiberSucceed((fiber) => typeof fiber.id) + * + * Effect.runSync(program) // => "number" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const withFiberSucceed: ( + evaluate: (fiber: Fiber) => A +) => Effect = core.withFiberSucceed + // ----------------------------------------------------------------------------- // Conversions // ----------------------------------------------------------------------------- @@ -3695,7 +3716,7 @@ export const orDie: (self: Effect) => Effect = in * output // => ["expected error: NetworkError", Exit.fail("NetworkError")] * ``` * - * @category sequencing + * @category error handling * @since 2.0.0 */ export const tapError: { @@ -3742,7 +3763,7 @@ export const tapError: { * output // => ["expected error: 504", Exit.fail(new NetworkError({ statusCode: 504 }))] * ``` * - * @category sequencing + * @category error handling * @since 2.0.0 */ export const tapErrorTag: { @@ -3797,7 +3818,7 @@ export const tapErrorTag: { * output // => ["Logging cause: Something went wrong", Exit.fail("Something went wrong")] * ``` * - * @category sequencing + * @category error handling * @since 4.0.0 */ export const tapCause: { @@ -3838,7 +3859,7 @@ export const tapCause: { * output // => ["Logging failure cause: Network timeout", Exit.fail("Network timeout")] * ``` * - * @category sequencing + * @category error handling * @since 4.0.0 */ export const tapCauseIf: { @@ -3872,7 +3893,7 @@ export const tapCauseIf: { * @see {@link tapCause} for observing every failure cause * @see {@link catchCauseFilter} for recovering from selected causes instead of only observing them * - * @category sequencing + * @category error handling * @since 4.0.0 */ export const tapCauseFilter: { @@ -3918,11 +3939,11 @@ export const tapCauseFilter: { * output // => ["defect: Something went wrong", Exit.die("Something went wrong")] * ``` * - * @category sequencing + * @category error handling * @since 2.0.0 */ export const tapDefect: { - (f: (defect: unknown) => Effect): (self: Effect) => Effect + (f: (defect: unknown) => Effect): (self: Effect) => Effect (self: Effect, f: (defect: unknown) => Effect): Effect } = internal.tapDefect @@ -3968,6 +3989,7 @@ export const eventually: (self: Effect) => Effect /** * Type helpers for retrying effects. * + * @category error handling * @since 2.0.0 */ export declare namespace Retry { @@ -4399,8 +4421,8 @@ export const withErrorReporting: < >( effectOrOptions: Arg, options?: { readonly defectsOnly?: boolean | undefined } | undefined -) => [Arg] extends [Effect] ? Arg : (self: Effect) => Effect = - internal.withErrorReporting +) => [Arg] extends [Effect] ? Effect + : (self: Effect) => Effect = internal.withErrorReporting // ----------------------------------------------------------------------------- // Fallback @@ -4412,8 +4434,9 @@ export const withErrorReporting: < * **Details** * * If the source effect succeeds, its value is preserved. If it fails in the - * error channel, `orElseSucceed` evaluates the fallback and succeeds with that - * value, removing the typed error from the returned effect. + * error channel, `orElseSucceed` evaluates the fallback with that error and + * succeeds with the returned value, removing the typed error from the returned + * effect. * * Defects and interruptions are not recovered by this operator. * @@ -4432,21 +4455,21 @@ export const withErrorReporting: < * } * } * - * const program = Effect.orElseSucceed(validate(-1), () => 18) + * const program = Effect.orElseSucceed(validate(-1), (error) => error === "IllegalAgeError" ? 18 : 0) * - * Effect.runSyncExit(program) // => Exit.succeed(18) + * Effect.runSyncExit(program) // => Exit.succeed(0) * ``` * * @category error handling * @since 2.0.0 */ export const orElseSucceed: { - ( - evaluate: LazyArg - ): (self: Effect) => Effect + ( + f: (error: NoInfer) => A2 + ): (self: Effect) => Effect ( self: Effect, - evaluate: LazyArg + f: (error: E) => A2 ): Effect } = internal.orElseSucceed @@ -4589,21 +4612,8 @@ export const timeoutOption: { } = internal.timeoutOption /** - * Applies a timeout to an effect, with a fallback effect executed if the timeout is reached. - * - * **When to use** - * - * Use when a timeout of an `Effect` should switch to a fallback effect. - * - * **Details** - * - * The fallback effect is created lazily by `orElse` and may introduce its own - * success, failure, and requirement types. - * - * **Gotchas** - * - * If the timeout wins, the source effect is interrupted before the fallback is - * run. + * Applies a timeout to an effect, lazily evaluating `orElse` after interrupting + * the source if the timeout is reached. * * **Example** (Falling back on timeout) * @@ -4936,7 +4946,7 @@ export const raceFirst: { * output // => [[2, 4], [2, 3]] * ``` * - * @category filtering + * @category collecting * @since 2.0.0 */ export const filter: { @@ -4981,7 +4991,7 @@ export const filter: { * @see {@link filter} for keeping original elements with a boolean predicate, refinement, or effectful predicate * @see {@link filterMapEffect} for using an effectful `Filter` * - * @category filtering + * @category collecting * @since 2.0.0 */ export const filterMap: { @@ -5015,7 +5025,7 @@ export const filterMap: { * @see {@link filterMap} for using a synchronous `Filter` * @see {@link filter} for keeping original elements with a predicate * - * @category filtering + * @category collecting * @since 4.0.0 */ export const filterMapEffect: { @@ -7112,8 +7122,8 @@ export const onExitFilter: { export const cached: (self: Effect) => Effect> = internal.cached /** - * Returns an effect that caches its result for a specified `Duration`, - * known as "timeToLive" (TTL). + * Returns an effect that caches its result for a fixed duration or a duration + * computed from its `Exit`, known as "timeToLive" (TTL). * * **When to use** * @@ -7132,6 +7142,17 @@ export const cached: (self: Effect) => Effect> * After the specified duration has passed, the cache expires, and the effect * will be recomputed upon the next evaluation. * + * `timeToLive` accepts a `Duration.Input` or a function from `Exit` to + * `Duration.Input`. The function runs once after each fresh computation, + * including failures, so successes and failures can have different TTLs. It + * does not run when the cache is created or when a cached result is reused. + * The callback also receives interruption exits, which are cached for the + * returned duration. + * + * The TTL starts when the computation completes. Concurrent callers share the + * pending computation. A zero TTL expires immediately, and an infinite TTL + * keeps the result indefinitely. + * * **Example** (Memoizing an effect with TTL) * * ```ts import.meta.vitest @@ -7156,6 +7177,26 @@ export const cached: (self: Effect) => Effect> * output // => ["expensive task...", "result 1", "result 1", "result 1"] * ``` * + * **Example** (Caching successes while retrying failures) + * + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * + * let attempts = 0 + * const task = Effect.suspend(() => + * ++attempts === 1 ? Effect.fail("temporary failure") : Effect.succeed(42) + * ) + * const program = Effect.gen(function*() { + * const cached = yield* task.pipe( + * Effect.cachedWithTTL((exit) => Exit.isSuccess(exit) ? "1 hour" : 0) + * ) + * yield* Effect.exit(cached) + * return yield* cached + * }) + * + * Effect.runSync(program) // => 42 + * ``` + * * @see {@link cached} for a similar function that caches the result * indefinitely. * @see {@link cachedInvalidateWithTTL} for a similar function that includes an @@ -7164,8 +7205,17 @@ export const cached: (self: Effect) => Effect> * @since 2.0.0 */ export const cachedWithTTL: { + ( + timeToLive: (exit: Exit.Exit) => Duration.Input + ): (self: Effect) => Effect> (timeToLive: Duration.Input): (self: Effect) => Effect> - (self: Effect, timeToLive: Duration.Input): Effect> + ( + timeToLive: Duration.Input | ((exit: Exit.Exit) => Duration.Input) + ): (self: Effect) => Effect> + ( + self: Effect, + timeToLive: Duration.Input | ((exit: Exit.Exit) => Duration.Input) + ): Effect> } = internal.cachedWithTTL /** @@ -7445,6 +7495,7 @@ export const abortSignal: Effect = internal.abortSign /** * Type helpers for repeating effects. * + * @category repetition * @since 2.0.0 */ export declare namespace Repeat { @@ -7661,10 +7712,10 @@ export const repeat: { * const program = Effect.repeatOrElse( * task, * Schedule.recurs(3), - * (error, attempts) => + * (error, previous) => * Effect.sync(() => { output.push( * `Final failure: ${error}, after ${ - * Option.getOrElse(attempts, () => 0) + * Option.isSome(previous) ? previous.value.attempt : 0 * } attempts` * ) }).pipe(Effect.map(() => 0)) * ) @@ -7679,12 +7730,12 @@ export const repeat: { export const repeatOrElse: { ( schedule: Schedule, - orElse: (error: E | E2, option: Option) => Effect + orElse: (error: E | E2, option: Option>) => Effect ): (self: Effect) => Effect ( self: Effect, schedule: Schedule, - orElse: (error: E | E2, option: Option) => Effect + orElse: (error: E | E2, option: Option>) => Effect ): Effect } = internalSchedule.repeatOrElse @@ -8686,7 +8737,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 sequencing + * @category forking * @since 2.0.0 */ export const awaitAllChildren: (self: Effect) => Effect = internal.awaitAllChildren @@ -9304,6 +9355,7 @@ export const runSyncExitWith: ( * * Use these to describe generator-based signatures and traced or untraced variants. * + * @category constructors * @since 3.11.0 */ export declare namespace fn { @@ -13893,15 +13945,18 @@ export const logTrace: (...message: ReadonlyArray) => Effect = intern * @category logging * @since 4.0.0 */ -export const withLogger = dual< +export const withLogger: { ( logger: Logger - ) => (effect: Effect) => Effect, + ): (effect: Effect) => Effect ( effect: Effect, logger: Logger - ) => Effect ->(2, (effect, logger) => + ): Effect +} = dual(2, ( + effect: Effect, + logger: Logger +): Effect => internal.updateService( effect, internal.CurrentLoggers, @@ -13943,28 +13998,24 @@ export const withLogger = dual< * @category logging * @since 2.0.0 */ -export const annotateLogs = dual< - { - ( - key: string, - value: unknown - ): (effect: Effect) => Effect - ( - values: Record - ): (effect: Effect) => Effect - }, - { - ( - effect: Effect, - key: string, - value: unknown - ): Effect - ( - effect: Effect, - values: Record - ): Effect - } ->( +export const annotateLogs: { + ( + key: string, + value: unknown + ): (effect: Effect) => Effect + ( + values: Record + ): (effect: Effect) => Effect + ( + effect: Effect, + key: string, + value: unknown + ): Effect + ( + effect: Effect, + values: Record + ): Effect +} = dual( (args) => isEffect(args[0]), ( effect: Effect, @@ -14060,12 +14111,12 @@ export const annotateLogsScoped: { * @category logging * @since 2.0.0 */ -export const withLogSpan = dual< - (label: string) => (effect: Effect) => Effect, - (effect: Effect, label: string) => Effect ->( +export const withLogSpan: { + (label: string): (effect: Effect) => Effect + (effect: Effect, label: string): Effect +} = dual( 2, - (effect, label) => + (effect: Effect, label: string): Effect => internal.flatMap(internal.currentTimeMillis, (now) => internal.updateService(effect, CurrentLogSpans, (spans) => { const span: [label: string, timestamp: number] = [label, now] @@ -14078,14 +14129,10 @@ export const withLogSpan = dual< // ----------------------------------------------------------------------------- /** - * Updates the `Metric` every time the `Effect` is executed. + * Updates a metric after each effect execution, optionally mapping its `Exit` to + * the metric's input. * - * **Details** - * - * Also accepts an optional function which can be used to map the `Exit` value - * of the `Effect` into a valid `Input` for the `Metric`. - * - * **Example** (Incrementing a metric for each execution) + * **Example** (Counting executions) * * ```ts import.meta.vitest * import { Effect, Metric } from "effect" @@ -14102,12 +14149,11 @@ export const withLogSpan = dual< * Effect.runSync(Metric.value(counter)).count // => 1 * ``` * - * **Example** (Mapping exits before updating a metric) + * **Example** (Mapping exits) * * ```ts import.meta.vitest * import { Effect, Exit, Metric } from "effect" * - * // Track different exit types with custom mapping * const exitTracker = Metric.frequency("exit_types", { * description: "Tracks success/failure/defect counts" * }) @@ -14132,7 +14178,7 @@ export const track: { ( metric: Metric.Metric, f: (exit: Exit.Exit) => Input - ): (self: Effect) => Effect + ): (self: Effect) => Effect ( metric: Metric.Metric, NoInfer>, State> ): (self: Effect) => Effect @@ -14497,7 +14543,7 @@ export const trackDuration: { * Effect.runSync(runnable) // => "Transaction complete" * ``` * - * @category services + * @category transactions * @since 4.0.0 */ export class Transaction extends Context.Service< @@ -14696,6 +14742,7 @@ export const txRetry: Effect = flatMap( /** * Type helpers for converting callback-based functions into `Effect` functions. * + * @category converting * @since 4.0.0 */ export declare namespace Effectify { @@ -14939,6 +14986,9 @@ export declare namespace Effectify { : never } +type EffectifyArgs) => any> = Parameters extends [...infer Args, any] ? Args + : Parameters + /** * Converts an error-first callback API into a function that returns an * `Effect`. @@ -14995,12 +15045,12 @@ export const effectify: { ) => any>(fn: F): Effectify.Effectify> ) => any, E>( fn: F, - onError: (error: Effectify.EffectifyError, args: Parameters) => E + onError: (error: Effectify.EffectifyError, args: EffectifyArgs) => E ): Effectify.Effectify ) => any, E, E2>( fn: F, - onError: (error: Effectify.EffectifyError, args: Parameters) => E, - onSyncError: (error: unknown, args: Parameters) => E2 + onError: (error: Effectify.EffectifyError, args: EffectifyArgs) => E, + onSyncError: (error: unknown, args: EffectifyArgs) => E2 ): Effectify.Effectify } = ((fn: Function, onError?: (e: any, args: any) => any, onSyncError?: (e: any, args: any) => any) => diff --git a/repos/effect/packages/effect/src/Effectable.ts b/repos/effect/packages/effect/src/Effectable.ts index f4a114ad8..b38518f65 100644 --- a/repos/effect/packages/effect/src/Effectable.ts +++ b/repos/effect/packages/effect/src/Effectable.ts @@ -1,6 +1,6 @@ /** * Low-level helpers for making custom values behave like Effects. The module - * exposes a prototype builder and an abstract base class that let + * exposes a prototype builder, an abstract base class, and a mixin that let * domain-specific values, such as service keys or configuration descriptions, * be evaluated by Effect and yielded inside `Effect.gen`. * @@ -24,6 +24,7 @@ import { evaluate, makePrimitiveProto } from "./internal/core.ts" * When the effect is evaluated, it calls `evaluate` with the current fiber. * * @see {@link Class} for a class-based approach to defining custom Effect values + * @see {@link Mixin} for wrapping an existing class constructor * * @category prototypes * @since 4.0.0 @@ -40,14 +41,16 @@ export const Prototype = >(options: { [evaluate]: options.evaluate }) as any +const proto = Prototype>({ + label: "Effectable", + evaluate(_) { + return this.asEffect() + } +}) + const Base: new() => Effect.Effect = (() => { const Base = function() {} - Base.prototype = Prototype({ - label: "Effectable", - evaluate(_) { - return this - } - }) + Base.prototype = proto return Base as any })() @@ -60,9 +63,75 @@ const Base: new() => Effect.Effect = (() => { * as `Effect` values. * * @see {@link Prototype} for a lower-level primitive approach to creating custom Effect-like values without a class + * @see {@link Mixin} for wrapping an existing class constructor * @category constructors * @since 2.0.0 */ export abstract class Class extends Base { - abstract override: Effect.Effect + abstract asEffect(): Effect.Effect +} + +type AsEffectReturn = Self extends { + asEffect(): infer A extends Effect.Effect +} ? A + : never + +declare abstract class MixinBase extends Class { + constructor(...args: ReadonlyArray) + override readonly [Effect.TypeId]: AsEffectReturn[Effect.TypeId] + override [Symbol.iterator](): Effect.EffectIterator> +} + +/** + * Returns a subclass of the provided class that inserts the Effect prototype + * into the inheritance chain. + * + * **When to use** + * + * Use to make instances of an existing class behave as `Effect` values without + * extending {@link Class} or modifying the original prototype. + * + * **Details** + * + * Pass the class to wrap, then implement `asEffect` on the final class. The + * returned class is abstract, and the success, error, and service types are + * inferred from the concrete `asEffect` return type. Concrete and abstract base + * classes are supported. Constructor parameters and instance members are + * preserved, except that Effect's prototype members shadow base prototype + * members with the same name: `pipe`, `toString`, `toJSON`, `[Symbol.iterator]`, + * and `[Symbol.for("nodejs.util.inspect.custom")]`. + * + * **Example** (Evaluating a mixed-in class) + * + * ```ts import.meta.vitest + * import { Effect, Effectable } from "effect" + * + * class Box { + * constructor(readonly value: number) {} + * } + * + * class EffectBox extends Effectable.Mixin(Box) { + * asEffect() { + * return Effect.succeed(this.value) + * } + * } + * + * const box = new EffectBox(2) + * Effect.isEffect(box) // => true + * await Effect.runPromise(box) // => 2 + * ``` + * + * @see {@link Prototype} for a lower-level primitive approach to creating custom Effect-like values without a class + * @see {@link Class} for a base constructor to extend + * @category constructors + * @since 4.0.0 + */ +export const Mixin = ) => object>( + klass: TBase +): TBase & typeof MixinBase => { + abstract class Mixed extends klass { + abstract asEffect(): Effect.Effect + } + Object.defineProperties(Mixed.prototype, Object.getOwnPropertyDescriptors(proto)) + return Mixed as TBase & typeof MixinBase } diff --git a/repos/effect/packages/effect/src/Encoding.ts b/repos/effect/packages/effect/src/Encoding.ts index f4b32aa89..25dcc1312 100644 --- a/repos/effect/packages/effect/src/Encoding.ts +++ b/repos/effect/packages/effect/src/Encoding.ts @@ -424,13 +424,104 @@ export const encodeHex: (input: Uint8Array | string) => string = (input) => * @since 4.0.0 */ export const randomHex = (length: number): string => { - let result = "" - for (let i = length >>> 3; i > 0; i--) { - const word = (Math.random() * 0x100000000) >>> 0 - result += byteToHex[word >>> 24] + byteToHex[(word >>> 16) & 0xff] + byteToHex[(word >>> 8) & 0xff] + - byteToHex[word & 0xff] + switch (length) { + case 16: + return randomHex16() + case 32: + return randomHex32() + default: { + let result = "" + for (let i = length >>> 3; i > 0; i--) { + result += randomHex8() + } + return result + } } - return result +} + +const hexCharCodes = Uint8Array.from("0123456789abcdef", (c) => c.charCodeAt(0)) + +const randomWord = (): number => (Math.random() * 0x100000000) >>> 0 + +// Trace and span identifiers are the common lengths. A single +// String.fromCharCode call produces a flat string, which avoids rope +// flattening when the identifier is later serialized. +const randomHex8 = (): string => { + const a = randomWord() + return String.fromCharCode( + hexCharCodes[a >>> 28], + hexCharCodes[(a >>> 24) & 15], + hexCharCodes[(a >>> 20) & 15], + hexCharCodes[(a >>> 16) & 15], + hexCharCodes[(a >>> 12) & 15], + hexCharCodes[(a >>> 8) & 15], + hexCharCodes[(a >>> 4) & 15], + hexCharCodes[a & 15] + ) +} + +const randomHex16 = (): string => { + const a = randomWord() + const b = randomWord() + return String.fromCharCode( + hexCharCodes[a >>> 28], + hexCharCodes[(a >>> 24) & 15], + hexCharCodes[(a >>> 20) & 15], + hexCharCodes[(a >>> 16) & 15], + hexCharCodes[(a >>> 12) & 15], + hexCharCodes[(a >>> 8) & 15], + hexCharCodes[(a >>> 4) & 15], + hexCharCodes[a & 15], + hexCharCodes[b >>> 28], + hexCharCodes[(b >>> 24) & 15], + hexCharCodes[(b >>> 20) & 15], + hexCharCodes[(b >>> 16) & 15], + hexCharCodes[(b >>> 12) & 15], + hexCharCodes[(b >>> 8) & 15], + hexCharCodes[(b >>> 4) & 15], + hexCharCodes[b & 15] + ) +} + +const randomHex32 = (): string => { + const a = randomWord() + const b = randomWord() + const c = randomWord() + const d = randomWord() + return String.fromCharCode( + hexCharCodes[a >>> 28], + hexCharCodes[(a >>> 24) & 15], + hexCharCodes[(a >>> 20) & 15], + hexCharCodes[(a >>> 16) & 15], + hexCharCodes[(a >>> 12) & 15], + hexCharCodes[(a >>> 8) & 15], + hexCharCodes[(a >>> 4) & 15], + hexCharCodes[a & 15], + hexCharCodes[b >>> 28], + hexCharCodes[(b >>> 24) & 15], + hexCharCodes[(b >>> 20) & 15], + hexCharCodes[(b >>> 16) & 15], + hexCharCodes[(b >>> 12) & 15], + hexCharCodes[(b >>> 8) & 15], + hexCharCodes[(b >>> 4) & 15], + hexCharCodes[b & 15], + hexCharCodes[c >>> 28], + hexCharCodes[(c >>> 24) & 15], + hexCharCodes[(c >>> 20) & 15], + hexCharCodes[(c >>> 16) & 15], + hexCharCodes[(c >>> 12) & 15], + hexCharCodes[(c >>> 8) & 15], + hexCharCodes[(c >>> 4) & 15], + hexCharCodes[c & 15], + hexCharCodes[d >>> 28], + hexCharCodes[(d >>> 24) & 15], + hexCharCodes[(d >>> 20) & 15], + hexCharCodes[(d >>> 16) & 15], + hexCharCodes[(d >>> 12) & 15], + hexCharCodes[(d >>> 8) & 15], + hexCharCodes[(d >>> 4) & 15], + hexCharCodes[d & 15] + ) } /** @@ -771,10 +862,7 @@ const base64UrlEncodeUint8Array = (data: Uint8Array) => // Hex internals -const byteToHex: Array = [] -for (let i = 0; i < 256; i++) { - byteToHex.push(i.toString(16).padStart(2, "0")) -} +const byteToHex: Array = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0")) const hexEncodeUint8Array = (bytes: Uint8Array): string => { let result = "" diff --git a/repos/effect/packages/effect/src/ExecutionPlan.ts b/repos/effect/packages/effect/src/ExecutionPlan.ts index 236004359..135385f76 100644 --- a/repos/effect/packages/effect/src/ExecutionPlan.ts +++ b/repos/effect/packages/effect/src/ExecutionPlan.ts @@ -289,6 +289,9 @@ const Proto: Omit, "steps"> = { return effect.contextWith((context: Context.Context) => effect.succeed(makeProto(self.steps.map((step) => ({ ...step, + while: step.while + ? (input: any) => effect.provideContext(step.while!(input), context) + : undefined, provide: Layer.isLayer(step.provide) ? Layer.provide(step.provide, Layer.succeedContext(context)) : step.provide diff --git a/repos/effect/packages/effect/src/Fiber.ts b/repos/effect/packages/effect/src/Fiber.ts index 4e511ee27..4eae1625c 100644 --- a/repos/effect/packages/effect/src/Fiber.ts +++ b/repos/effect/packages/effect/src/Fiber.ts @@ -13,12 +13,13 @@ import type { Effect } from "./Effect.ts" import type { Exit } from "./Exit.ts" import * as effect from "./internal/effect.ts" import type { LogLevel } from "./LogLevel.ts" +import type { FiberRuntimeMetricsService } from "./Metric.ts" import type { Pipeable } from "./Pipeable.ts" import { hasProperty } from "./Predicate.ts" import type { StackFrame } from "./References.ts" import type { Scheduler, SchedulerDispatcher } from "./Scheduler.ts" import type { Scope } from "./Scope.ts" -import type { AnySpan } from "./Tracer.ts" +import type { AnySpan, Tracer } from "./Tracer.ts" import type { Covariant } from "./Types.ts" const TypeId = "~effect/Fiber" @@ -75,14 +76,8 @@ export interface Fiber extends Pipeable { readonly getRef: (ref: Context.Reference) => A readonly context: Context.Context setContext(context: Context.Context): void - readonly currentScheduler: Scheduler + readonly cache: Fiber.Cache readonly currentDispatcher: SchedulerDispatcher - readonly currentSpan?: AnySpan | undefined - readonly currentLogLevel: LogLevel - readonly minimumLogLevel: LogLevel - readonly currentStackFrame?: StackFrame | undefined - readonly maxOpsBeforeYield: number - readonly currentPreventYield: boolean readonly addObserver: (cb: (exit: Exit) => void) => () => void readonly interruptUnsafe: ( fiberId?: number | undefined, @@ -155,6 +150,36 @@ export declare namespace Fiber { readonly _A: Covariant readonly _E: Covariant } + + /** + * Context-derived values cached for the fiber's current `Context`. + * + * **When to use** + * + * Use to read runtime services resolved from the fiber's context, such as + * the scheduler, current span, or log levels. + * + * **Details** + * + * The cache object is computed once per context cache root and shared by + * every fiber running with that root, so it must be treated as immutable. + * + * @category models + * @since 4.0.0 + */ + export interface Cache { + readonly scheduler: Scheduler + readonly tracer: Tracer | undefined + readonly tracerContext: Tracer["context"] | undefined + readonly tracerEnabled: boolean + readonly span: AnySpan | undefined + readonly logLevel: LogLevel + readonly minimumLogLevel: LogLevel + readonly stackFrame: StackFrame | undefined + readonly runtimeMetrics: FiberRuntimeMetricsService | undefined + readonly maxOpsBeforeYield: number + readonly preventYield: boolean + } } const await_: (self: Fiber) => Effect> = effect.fiberAwait diff --git a/repos/effect/packages/effect/src/FiberHandle.ts b/repos/effect/packages/effect/src/FiberHandle.ts index 7a004bfa1..2317b7a42 100644 --- a/repos/effect/packages/effect/src/FiberHandle.ts +++ b/repos/effect/packages/effect/src/FiberHandle.ts @@ -332,10 +332,10 @@ export const setUnsafe: { fiber.interruptUnsafe(internalFiberId) return } else if (self.state.fiber !== undefined) { - if (options?.onlyIfMissing === true) { - fiber.interruptUnsafe(internalFiberId) + if (self.state.fiber === fiber) { return - } else if (self.state.fiber === fiber) { + } else if (options?.onlyIfMissing === true) { + fiber.interruptUnsafe(internalFiberId) return } self.state.fiber.interruptUnsafe(internalFiberId) diff --git a/repos/effect/packages/effect/src/FiberMap.ts b/repos/effect/packages/effect/src/FiberMap.ts index 828632ac5..a8b7f1fbe 100644 --- a/repos/effect/packages/effect/src/FiberMap.ts +++ b/repos/effect/packages/effect/src/FiberMap.ts @@ -305,7 +305,8 @@ const isInternalInterruption = Filter.toPredicate(Filter.compose( * * When the fiber completes, it is removed from the map. If the key already has * a fiber, that previous fiber is interrupted unless `onlyIfMissing` is set; - * in that case the new fiber is interrupted and the existing entry is kept. + * in that case a different new fiber is interrupted and the existing entry is + * kept, while re-registering the existing fiber is a no-op. * * **Example** (Adding a fiber unsafely) * @@ -367,16 +368,19 @@ export const setUnsafe: { const previous = MutableHashMap.get(self.state.backing, key) if (previous._tag === "Some") { - if (options?.onlyIfMissing === true) { - fiber.interruptUnsafe(internalFiberId) + if (previous.value === fiber) { return - } else if (previous.value === fiber) { + } else if (options?.onlyIfMissing === true) { + fiber.interruptUnsafe(internalFiberId) return } - previous.value.interruptUnsafe(internalFiberId) } + // Install the replacement before interruption can re-enter the map through a finalizer. MutableHashMap.set(self.state.backing, key, fiber) + if (previous._tag === "Some") { + previous.value.interruptUnsafe(internalFiberId) + } fiber.addObserver((exit) => { if (self.state._tag === "Closed") { return @@ -405,7 +409,8 @@ export const setUnsafe: { * * When the fiber completes, it is removed from the map. If the key already has * a fiber, that previous fiber is interrupted unless `onlyIfMissing` is set; - * in that case the new fiber is interrupted and the existing entry is kept. + * in that case a different new fiber is interrupted and the existing entry is + * kept, while re-registering the existing fiber is a no-op. * * This is the Effect-wrapped version of `setUnsafe`. * diff --git a/repos/effect/packages/effect/src/FileSystem.ts b/repos/effect/packages/effect/src/FileSystem.ts index 03d02dc18..9ea4cf350 100644 --- a/repos/effect/packages/effect/src/FileSystem.ts +++ b/repos/effect/packages/effect/src/FileSystem.ts @@ -5,13 +5,13 @@ * Platform packages provide concrete layers, while this module defines the * operations for reading, writing, inspecting, streaming, and watching files. * Operations return `Effect`, `Stream`, or `Sink` values and fail with - * `PlatformError`. The module also includes file handles, size helpers, open - * flags, watch events, and the watch backend service. + * `PlatformError`. The module also includes file handles, open flags, watch + * events, and the watch backend service. * * @since 4.0.0 */ import * as Arr from "./Array.ts" -import type * as Brand from "./Brand.ts" +import * as ByteSize from "./ByteSize.ts" import * as Cause from "./Cause.ts" import * as Context from "./Context.ts" import * as Effect from "./Effect.ts" @@ -39,12 +39,12 @@ const TypeId = "~effect/FileSystem" * **Example** (Accessing file system operations) * * ```ts import.meta.vitest - * import { Effect, FileSystem } from "effect" + * import { ByteSize, 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), + * stat: () => Effect.succeed({ size: ByteSize.bytes(22) } as FileSystem.File.Info), * readFileString: () => Effect.succeed("{\"env\": \"development\"}") * }) * @@ -68,7 +68,7 @@ const TypeId = "~effect/FileSystem" * }) * * const result = Effect.runSync(Effect.provideService(program, FileSystem.FileSystem, fileSystem)) - * result.size // => 22n + * ByteSize.toBigInt(result.size) // => 22n * result.content // => "{\"env\": \"development\"}" * ``` * @@ -299,7 +299,7 @@ export interface FileSystem { } ) => Sink.Sink /** - * Get information about a file at `path`. + * Get information about a file at `path`. See `File.Info` for metadata limits. */ readonly stat: ( path: string @@ -321,9 +321,9 @@ export interface FileSystem { readonly stream: ( path: string, options?: { - readonly bytesToRead?: SizeInput | undefined - readonly chunkSize?: SizeInput | undefined - readonly offset?: SizeInput | undefined + readonly bytesToRead?: ByteSize.Input | undefined + readonly chunkSize?: number | undefined + readonly offset?: ByteSize.Input | undefined } ) => Stream.Stream /** @@ -339,7 +339,7 @@ export interface FileSystem { */ readonly truncate: ( path: string, - length?: SizeInput + length?: number ) => Effect.Effect /** * Change the file system timestamps of the file at `path`. @@ -383,199 +383,6 @@ export interface FileSystem { ) => Effect.Effect } -/** - * Represents a file size in bytes using a branded bigint. - * - * **Details** - * - * This type ensures type safety when working with file sizes, preventing - * accidental mixing of regular numbers with size values. The underlying - * bigint allows for handling very large file sizes beyond JavaScript's - * number precision limits. - * - * **Example** (Creating branded file sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.Size(1024) // => 1024n - * FileSystem.Size(BigInt("9007199254740992")) // => 9007199254740992n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export type Size = Brand.Branded - -/** - * Input type for size parameters that accepts multiple numeric types. - * - * **Details** - * - * This union type allows file system operations to accept size values in - * different formats for convenience, which are then normalized to the - * branded `Size` type internally. - * - * **Example** (Using size inputs) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * const inputs: ReadonlyArray = [ - * 1024, - * 2048n, - * FileSystem.Size(4096) - * ] - * inputs.map(FileSystem.Size) // => [1024n, 2048n, 4096n] - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export type SizeInput = bigint | number | Size - -/** - * Creates a `Size` from various numeric input types. - * - * **Details** - * - * Converts numbers, bigints, or existing Size values into a properly - * branded Size type. This function handles the conversion and ensures - * type safety for file size operations. - * - * **Example** (Converting size inputs) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * // From number - * const size1 = FileSystem.Size(1024) - * typeof size1 // => "bigint" - * - * // From bigint - * const size2 = FileSystem.Size(BigInt(2048)) - * - * // From existing Size (identity) - * const size3 = FileSystem.Size(size1) - * const sizes = [size2, size3] // => [2048n, 1024n] - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const Size = (bytes: SizeInput): Size => typeof bytes === "bigint" ? bytes as Size : BigInt(bytes) as Size - -/** - * Creates a `Size` representing kilobytes (1024 bytes). - * - * **Details** - * - * Converts a number of kilobytes to the equivalent size in bytes. - * Uses binary kilobytes (1024 bytes) rather than decimal (1000 bytes). - * - * **Example** (Creating kibibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.KiB(64) // => 65536n - * FileSystem.KiB(100) // => 102400n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const KiB = (n: number): Size => Size(n * 1024) - -/** - * Creates a `Size` representing mebibytes (1024² bytes). - * - * **Details** - * - * Converts a number of mebibytes to the equivalent size in bytes. - * Uses binary mebibytes (1,048,576 bytes) rather than decimal megabytes. - * - * **Example** (Creating mebibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.MiB(10) // => 10485760n - * FileSystem.MiB(100) // => 104857600n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const MiB = (n: number): Size => Size(n * 1024 * 1024) - -/** - * Creates a `Size` representing gibibytes (1024³ bytes). - * - * **Details** - * - * Converts a number of gibibytes to the equivalent size in bytes. - * Uses binary gibibytes (1,073,741,824 bytes) rather than decimal gigabytes. - * - * **Example** (Creating gibibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.GiB(1) // => 1073741824n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const GiB = (n: number): Size => Size(n * 1024 * 1024 * 1024) - -/** - * Creates a `Size` representing tebibytes (1024⁴ bytes). - * - * **Details** - * - * Converts a number of tebibytes to the equivalent size in bytes. - * Uses binary tebibytes (1,099,511,627,776 bytes) rather than decimal terabytes. - * - * **Example** (Creating tebibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.TiB(1) // => 1099511627776n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const TiB = (n: number): Size => Size(n * 1024 * 1024 * 1024 * 1024) - -const bigint1024 = BigInt(1024) -const bigintPiB = bigint1024 * bigint1024 * bigint1024 * bigint1024 * bigint1024 - -/** - * Creates a `Size` representing pebibytes (1024⁵ bytes). - * - * **Details** - * - * Converts a number of pebibytes to the equivalent size in bytes. - * Uses binary pebibytes (1,125,899,906,842,624 bytes) rather than decimal petabytes. - * This function uses BigInt arithmetic to handle the very large numbers involved. - * - * **Example** (Creating pebibyte sizes) - * - * ```ts import.meta.vitest - * import { FileSystem } from "effect" - * - * FileSystem.PiB(2) // => 2251799813685248n - * ``` - * - * @category sizes - * @since 4.0.0 - */ -export const PiB = (n: number): Size => Size(BigInt(n) * bigintPiB) - /** * File open flags that determine how a file is opened and what operations are allowed. * @@ -660,7 +467,7 @@ export type OpenFlag = * @category services * @since 4.0.0 */ -export const FileSystem: Context.Service = Context.Service("effect/platform/FileSystem") +export const FileSystem: Context.Service = Context.Service("effect/FileSystem") /** * Creates a FileSystem implementation from a partial implementation. @@ -712,12 +519,16 @@ export const make = ( })), stream: Effect.fnUntraced(function*(path, options) { const file = yield* impl.open(path, { flag: "r" }) - if (options?.offset) { - yield* file.seek(options.offset, "start") + const offset = options?.offset === undefined ? undefined : ByteSize.fromInputUnsafe(options.offset) + if (offset) { + yield* file.seek(offset, "start") } - const bytesToRead = options?.bytesToRead !== undefined ? Size(options.bytesToRead) : undefined + const bytesToRead = options?.bytesToRead === undefined + ? undefined + : ByteSize.fromInputUnsafe(options.bytesToRead) let totalBytesRead = BigInt(0) - const chunkSize = Size(options?.chunkSize ?? 64 * 1024) + // Validate chunk sizes even for zero-byte reads. + const chunkSize = Number(BigInt(options?.chunkSize ?? 64 * 1024)) const readChunk = file.readAlloc(chunkSize) return Stream.fromPull(Effect.succeed( Effect.flatMap( @@ -726,7 +537,7 @@ export const make = ( return Cause.done() } return bytesToRead !== undefined && (bytesToRead - totalBytesRead) < chunkSize - ? file.readAlloc(bytesToRead - totalBytesRead) + ? file.readAlloc(Number(bytesToRead - totalBytesRead)) : readChunk }), Option.match({ @@ -741,7 +552,7 @@ export const make = ( }, Stream.unwrap), sink: (path, options) => pipe( - impl.open(path, { flag: "w", ...options }), + impl.open(path, { ...options, flag: options?.flag ?? "w" }), Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))), Sink.unwrap ), @@ -1005,20 +816,20 @@ export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) * **Example** (Working with file handles) * * ```ts import.meta.vitest - * import { Effect, FileSystem, Option } from "effect" + * import { ByteSize, 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)), + * stat: Effect.succeed({ size: ByteSize.bytes(5) } as FileSystem.File.Info), + * seek: () => Effect.succeed(BigInt(0)), * sync: Effect.void, * read: (buffer) => Effect.sync(() => { * buffer.set([1, 2, 3, 4, 5]) - * return FileSystem.Size(5) + * return 5 * }), * readAlloc: () => Effect.succeed(Option.none()), * truncate: () => Effect.void, - * write: (buffer) => Effect.succeed(FileSystem.Size(buffer.length)), + * write: (buffer) => Effect.succeed(buffer.length), * writeAll: () => Effect.void * } * @@ -1031,7 +842,10 @@ export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) * return { size: stats.size, bytesRead, buffer: Array.from(buffer) } * }) * - * Effect.runSync(program) // => { size: 5n, bytesRead: 5n, buffer: [1, 2, 3, 4, 5] } + * const result = Effect.runSync(program) + * ByteSize.toBigInt(result.size) // => 5n + * result.bytesRead // => 5 + * result.buffer // => [1, 2, 3, 4, 5] * ``` * * @category models @@ -1039,13 +853,19 @@ export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) */ export interface File { readonly [FileTypeId]: typeof FileTypeId + /** + * Get information about the open file. See `File.Info` for metadata limits. + */ readonly stat: Effect.Effect - readonly seek: (offset: SizeInput, from: SeekMode) => Effect.Effect + /** + * Seeks before the start fail with `BadArgument` and leave the cursor unchanged. + */ + readonly seek: (offset: bigint, from: SeekMode) => Effect.Effect readonly sync: Effect.Effect - readonly read: (buffer: Uint8Array) => Effect.Effect - readonly readAlloc: (size: SizeInput) => Effect.Effect, PlatformError> - readonly truncate: (length?: SizeInput) => Effect.Effect - readonly write: (buffer: Uint8Array) => Effect.Effect + readonly read: (buffer: Uint8Array) => Effect.Effect + readonly readAlloc: (size: number) => Effect.Effect, PlatformError> + readonly truncate: (length?: number) => Effect.Effect + readonly write: (buffer: Uint8Array) => Effect.Effect readonly writeAll: (buffer: Uint8Array) => Effect.Effect } @@ -1086,10 +906,14 @@ export declare namespace File { * permissions, and size information. This structure is returned by file * stat operations. * + * Node and Bun preserve `size` and `blksize` exactly. Optional `number` + * metadata is `Option.none()` when absent or outside the safe integer range. + * Unsafe `dev` or `mode` values fail the stat operation with `BadArgument`. + * * **Example** (Inspecting file information) * * ```ts import.meta.vitest - * import { FileSystem, Option } from "effect" + * import { ByteSize, FileSystem, Option } from "effect" * * const info: FileSystem.File.Info = { * type: "File", @@ -1103,13 +927,13 @@ export declare namespace File { * uid: Option.none(), * gid: Option.none(), * rdev: Option.none(), - * size: FileSystem.Size(5), + * size: ByteSize.bytes(5), * blksize: Option.none(), * blocks: Option.none() * } * * info.type // => "File" - * info.size // => 5n + * ByteSize.toBigInt(info.size) // => 5n * info.mode.toString(8) // => "644" * * const modified = Option.match(info.mtime, { @@ -1135,8 +959,8 @@ export declare namespace File { readonly uid: Option.Option readonly gid: Option.Option readonly rdev: Option.Option - readonly size: Size - readonly blksize: Option.Option + readonly size: ByteSize.ByteSize + readonly blksize: Option.Option readonly blocks: Option.Option } } @@ -1296,4 +1120,4 @@ export class WatchBackend extends Context.Service Option.Option> -}>()("effect/platform/FileSystem/WatchBackend") {} +}>()("effect/FileSystem/WatchBackend") {} diff --git a/repos/effect/packages/effect/src/Formatter.ts b/repos/effect/packages/effect/src/Formatter.ts index 915cca9df..9121c252d 100644 --- a/repos/effect/packages/effect/src/Formatter.ts +++ b/repos/effect/packages/effect/src/Formatter.ts @@ -169,7 +169,7 @@ export function format(input: unknown, options?: { v["toString"] !== Array.prototype.toString ) { const s = safeToString(v) - output = v instanceof Error && v.cause ? `${s} (cause: ${recur(v.cause, d)})` : s + output = v instanceof Error && v.cause !== undefined ? `${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 { diff --git a/repos/effect/packages/effect/src/Graph.ts b/repos/effect/packages/effect/src/Graph.ts index b1b94c533..7f9ae698b 100644 --- a/repos/effect/packages/effect/src/Graph.ts +++ b/repos/effect/packages/effect/src/Graph.ts @@ -19,7 +19,6 @@ import * as csr from "./internal/graphCsr.ts" import * as MutableHashMap from "./MutableHashMap.ts" import * as Option from "./Option.ts" import type { Pipeable } from "./Pipeable.ts" -import { hasProperty } from "./Predicate.ts" import type { Covariant, Invariant } from "./Types.ts" const TypeId = internal.TypeId @@ -147,22 +146,6 @@ export interface Snapshot { readonly edges: ReadonlyArray> } -/** - * Common public protocol for graph values. - * - * **Details** - * - * Contains only the runtime marker and shared protocols. Graph storage is kept - * internal; use module functions such as `nodes`, `edges`, `getNode`, and - * `getEdge` to inspect graph contents. - * - * @category protocols - * @since 3.18.0 - */ -export interface Proto extends Iterable, Equal.Equal, Pipeable, Inspectable { - readonly [TypeId]: Graph.Variance -} - /** * Immutable graph interface. * @@ -181,7 +164,10 @@ export interface Proto extends Iterable, * @category models * @since 3.18.0 */ -export interface Graph extends Proto { +export interface Graph + extends Iterable, Equal.Equal, Pipeable, Inspectable +{ + readonly [TypeId]: Graph.Variance readonly type: T readonly mutable: false } @@ -415,9 +401,9 @@ const withMutationGuard = ( * @category guards * @since 4.0.0 */ -export const isGraph = ( +export const isGraph: ( u: U | Graph | MutableGraph -): u is Graph | MutableGraph => hasProperty(u, TypeId) +) => u is Graph | MutableGraph = internal.isGraph /** * Reconstructs an immutable graph from its indexed active structure. @@ -6941,7 +6927,7 @@ export const bellmanFord: { const edges = csr.getEdges(cache) const edgeIds = csr.getEdgeIds(cache) const edgeCache = csr.getEdgeEndpoints(cache) - const outgoing = csr.getOutgoing(cache) + const outgoing = csr.getOutgoingWithEdges(cache) const source = csr.getNodeIndex(cache, config.source)! const target = csr.getNodeIndex(cache, config.target)! const weights = new Float64Array(edges.length) @@ -7034,7 +7020,9 @@ export const bellmanFord: { while (head < tail) { const node = queue[head++] for (let i = outgoing.rowOffsets[node]; i < outgoing.rowOffsets[node + 1]; i++) { - markAffected(outgoing.columnIndices[i]) + if (weights[outgoing.edgeIndices[i]] !== Infinity) { + markAffected(outgoing.columnIndices[i]) + } } } } diff --git a/repos/effect/packages/effect/src/Hash.ts b/repos/effect/packages/effect/src/Hash.ts index 797c61fcf..99f226526 100644 --- a/repos/effect/packages/effect/src/Hash.ts +++ b/repos/effect/packages/effect/src/Hash.ts @@ -106,10 +106,6 @@ export const hash: (self: A) => number = (self: A) => { return number(self) case "bigint": return string(self.toString(10)) - case "boolean": - return string(String(self)) - case "symbol": - return string(String(self)) case "string": return string(self) case "undefined": @@ -153,9 +149,8 @@ export const hash: (self: A) => number = (self: A) => { } } default: - throw new Error( - `BUG: unhandled typeof ${typeof self} - please report an issue at https://github.com/Effect-TS/effect/issues` - ) + // The remaining primitive types are boolean and symbol. + return string(String(self)) } } @@ -318,14 +313,8 @@ export const isHash = (u: unknown): u is Hash => hasProperty(u, symbol) * @since 2.0.0 */ export const number = (n: number) => { - if (n !== n) { - return string("NaN") - } - if (n === Infinity) { - return string("Infinity") - } - if (n === -Infinity) { - return string("-Infinity") + if (n !== n || n === Infinity || n === -Infinity) { + return string(String(n)) } let h = n | 0 if (h !== n) { diff --git a/repos/effect/packages/effect/src/HashRing.ts b/repos/effect/packages/effect/src/HashRing.ts index 490832a97..789bf0500 100644 --- a/repos/effect/packages/effect/src/HashRing.ts +++ b/repos/effect/packages/effect/src/HashRing.ts @@ -420,7 +420,7 @@ function getIndexForInput( return [a, distA] } const range = Math.max(lo, len - lo) - for (let i = 1; i < range; i++) { + for (let i = 1; i <= range; i++) { let index = lo - i if (index >= 0 && index < len && !exclude.has(ring[index][1])) { return [index, Math.abs(ring[index][0] - hash)] diff --git a/repos/effect/packages/effect/src/JsonPatch.ts b/repos/effect/packages/effect/src/JsonPatch.ts index 605d24e2b..8d0943dd3 100644 --- a/repos/effect/packages/effect/src/JsonPatch.ts +++ b/repos/effect/packages/effect/src/JsonPatch.ts @@ -307,8 +307,8 @@ function tokenize(pointer: string): Array { return pointer.split("/").slice(1).map(unescapeToken) } -/** Convert a reference token to a non-negative array index (rejects `-` and negatives). */ -function toIndex(token: string): number { +/** Converts a JSON Pointer reference token to a non-negative array index. */ +function toJsonPointerArrayIndex(token: string): number { if (!/^(0|[1-9]\d*)$/.test(token)) { throw new Error(`Invalid array index: "${token}"`) } @@ -332,7 +332,7 @@ function applyOperation(doc: Schema.Json, op: JsonPatchOperation): Schema.Json { 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 index = lastToken === "-" ? parent.length : toJsonPointerArrayIndex(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() @@ -374,7 +374,7 @@ function resolveParent( const token = tokens[i] if (Array.isArray(cur)) { - const idx = toIndex(token) + const idx = toJsonPointerArrayIndex(token) if (idx >= cur.length) return null stack.push({ container: cur, token: idx }) cur = cur[idx] diff --git a/repos/effect/packages/effect/src/JsonPointer.ts b/repos/effect/packages/effect/src/JsonPointer.ts index 385d578b6..78392b9da 100644 --- a/repos/effect/packages/effect/src/JsonPointer.ts +++ b/repos/effect/packages/effect/src/JsonPointer.ts @@ -1,8 +1,8 @@ /** - * Helpers for escaping and unescaping JSON Pointer path segments. JSON Pointer - * uses `/` to separate path tokens inside a JSON document, so token text must - * encode literal `~` and `/` characters. This module provides the two RFC 6901 - * token conversions used by JSON Patch and related path handling. + * Helpers for escaping JSON Pointer path segments and converting JSON Pointer + * URI fragments. JSON Pointer uses `/` to separate path tokens inside a JSON + * document, so token text must encode literal `~` and `/` characters. URI + * fragments additionally apply percent-encoding after JSON Pointer escaping. * * @since 4.0.0 */ @@ -78,3 +78,96 @@ export function escapeToken(token: string): string { export function unescapeToken(token: string): string { return token.replace(/~1/g, "/").replace(/~0/g, "~") } + +/** @internal */ +export function formatUriFragmentToken(token: string): string { + return encodeURI(escapeToken(token)).replace(/#/g, "%23") +} + +/** @internal */ +export function decodeUriFragment(fragment: string): string | undefined { + if (fragment.length === 0 || fragment === "#") return "" + if (!fragment.startsWith("#")) return undefined + const encoded = fragment.slice(1) + try { + if (encodeURI(encoded).replace(/%25/g, "%").replace(/#/g, "%23") !== encoded) return undefined + const pointer = decodeURIComponent(encoded) + return pointer.startsWith("/") && !/~(?:[^01]|$)/.test(pointer) ? pointer : undefined + } catch { + return undefined + } +} + +/** + * Parses a JSON Pointer URI fragment into decoded path tokens. + * + * **When to use** + * + * Use when you need to resolve a URI fragment against a JSON document. + * + * **Details** + * + * Percent-encoding is decoded before the pointer is split into tokens, then + * each token is decoded with {@link unescapeToken}. The empty string and `#` + * both represent the document root. + * + * **Gotchas** + * + * Returns `undefined` when the input is not a URI fragment, contains characters + * that require percent-encoding, or contains an invalid JSON Pointer escape + * sequence. + * + * **Example** (Parsing URI fragments) + * + * ```ts import.meta.vitest + * import { JsonPointer } from "effect" + * + * JsonPointer.parseUriFragment("#/users/a~1b") // => ["users", "a/b"] + * JsonPointer.parseUriFragment("#/caf%C3%A9") // => ["café"] + * JsonPointer.parseUriFragment("#/%") // => undefined + * JsonPointer.parseUriFragment("#/a#b") // => undefined + * ``` + * + * @see {@link formatUriFragment} for the inverse operation + * @category decoding + * @since 4.0.0 + */ +export function parseUriFragment(fragment: string): ReadonlyArray | undefined { + const pointer = decodeUriFragment(fragment) + return pointer === undefined ? undefined : pointer.length === 0 ? [] : pointer.slice(1).split("/").map(unescapeToken) +} + +/** + * Formats path tokens as a JSON Pointer URI fragment. + * + * **When to use** + * + * Use when you need a URI fragment that identifies a value in a JSON document. + * + * **Details** + * + * Each token is encoded with {@link escapeToken} before URI percent-encoding + * is applied. An empty path is formatted as `#`. + * + * **Gotchas** + * + * Throws a `URIError` when a token contains an unpaired surrogate. + * + * **Example** (Formatting a URI fragment) + * + * ```ts import.meta.vitest + * import { JsonPointer } from "effect" + * + * JsonPointer.formatUriFragment(["users", "a/b", "Rate%"]) // => "#/users/a~1b/Rate%25" + * ``` + * + * @see {@link parseUriFragment} for the inverse operation + * @category encoding + * @since 4.0.0 + */ +export function formatUriFragment(path: ReadonlyArray): string { + return path.reduce( + (fragment, token) => `${fragment}/${formatUriFragmentToken(token)}`, + "#" + ) +} diff --git a/repos/effect/packages/effect/src/JsonSchema.ts b/repos/effect/packages/effect/src/JsonSchema.ts index 054b8e6b6..5cbd69837 100644 --- a/repos/effect/packages/effect/src/JsonSchema.ts +++ b/repos/effect/packages/effect/src/JsonSchema.ts @@ -9,7 +9,7 @@ * @since 4.0.0 */ import * as InternalRecord from "./internal/record.ts" -import { escapeToken, unescapeToken } from "./JsonPointer.ts" +import { formatUriFragment, parseUriFragment } from "./JsonPointer.ts" import * as Predicate from "./Predicate.ts" /** @@ -218,9 +218,9 @@ function isMetaSchemaUri(value: unknown, uri: string): boolean { } function rewriteOpenApiComponentsReference(reference: string): string { - const path = reference.startsWith("#") ? parsePointerFragment(reference) : undefined + const path = reference.startsWith("#") ? parseUriFragment(reference) : undefined return path !== undefined && path[0] === "components" && path[1] === "schemas" - ? formatPointerFragment(["$defs", ...path.slice(2)]) + ? formatUriFragment(["$defs", ...path.slice(2)]) : reference } @@ -627,7 +627,7 @@ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-20 return transformSchema(schema, (schema, inEmbeddedResource) => { rejectKeywordCollisions(schema, OPEN_API_31_TARGET_COLLISIONS, "OpenAPI 3.1", "Draft 2020-12") rewriteSchemaRef(schema, (reference, keyword) => { - const path = reference.startsWith("#") ? parsePointerFragment(reference) : undefined + const path = reference.startsWith("#") ? parseUriFragment(reference) : undefined if (path === undefined || path[0] !== "$defs" || path.length < 2) return reference const key = path[1] if (isRootResource) { @@ -643,7 +643,7 @@ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-20 } return inEmbeddedResource ? reference - : formatPointerFragment(["components", "schemas", keyMap.get(key) ?? key, ...path.slice(2)]) + : formatUriFragment(["components", "schemas", keyMap.get(key) ?? key, ...path.slice(2)]) }) }) as JsonSchema } @@ -682,7 +682,7 @@ export function sanitizeOpenApiComponentsSchemasKey(s: string): string { /** @internal */ export function getReferenceKey($ref: string): string | undefined { - const path = $ref.startsWith("#") ? parsePointerFragment($ref) : undefined + const path = $ref.startsWith("#") ? parseUriFragment($ref) : undefined return path !== undefined && path.length === 2 && path[0] === "$defs" ? path[1] : undefined @@ -815,7 +815,7 @@ function runConverter(adapter: Adapter, options: ConverterOptions | undefined let reference = value const resolved = resolveUrl(value, sourceResource) if (resolved !== undefined) { - const sourcePointer = parsePointerFragment(resolved.hash) + const sourcePointer = parseUriFragment(resolved.hash) resolved.hash = "" if (sourcePointer !== undefined) { const targetPath = locations.get(locationKey(resolved.href, sourcePointer)) @@ -927,29 +927,11 @@ function resolveResourceUri(value: unknown, base: string): string | undefined { return url.href } -function parsePointerFragment(hash: string): Path | undefined { - if (hash.length === 0) return [] - let pointer: string - try { - pointer = decodeURIComponent(hash.slice(1)) - } catch { - return undefined - } - if (!pointer.startsWith("/")) return undefined - return /~(?:[^01]|$)/.test(pointer) ? undefined : pointer.slice(1).split("/").map(unescapeToken) -} - function relocateReference(reference: string, targetPath: Path): string { const index = reference.indexOf("#") if (index === -1 && targetPath.length === 0) return reference const uri = index === -1 ? reference : reference.slice(0, index) - return `${uri}${formatPointerFragment(targetPath)}` -} - -function formatPointerFragment(path: Path): string { - return path.length === 0 - ? "#" - : `#/${path.map((token) => encodeURI(escapeToken(token)).replace(/#/g, "%23")).join("/")}` + return `${uri}${formatUriFragment(targetPath)}` } function locationKey(resource: string, pointer: Path): string { diff --git a/repos/effect/packages/effect/src/Layer.ts b/repos/effect/packages/effect/src/Layer.ts index ede9373c1..c5bd8f629 100644 --- a/repos/effect/packages/effect/src/Layer.ts +++ b/repos/effect/packages/effect/src/Layer.ts @@ -1137,6 +1137,8 @@ export const effectDiscard = (effect: Effect): Layer(evaluate: LazyArg>): Layer => fromBuildMemo((memoMap, scope) => internalEffect.suspend(() => evaluate().build(memoMap, scope))) +const unwrapKey = Context.Service>("effect/Layer/unwrap") + /** * Unwraps a `Layer` from an `Effect`, flattening the nested structure. * @@ -1173,10 +1175,7 @@ export const suspend = (evaluate: LazyArg>): Layer( self: Effect, E, R> -): Layer> => { - const service = Context.Service>("effect/Layer/unwrap") - return flatMap(effect(service)(self), Context.get(service)) -} +): Layer> => flatMap(effect(unwrapKey)(self), Context.get(unwrapKey)) const mergeAllEffect = , ...Array>]>( layers: Layers, @@ -1738,21 +1737,24 @@ export const tap: { * @since 2.0.0 */ export const tapError: { - ( - f: (e: XE) => Effect + ( + f: (e: Types.NoInfer) => Effect ): (self: Layer) => Layer> - ( + ( + f: (e: E) => Effect + ): (self: Layer) => Layer> + ( self: Layer, - f: (e: XE) => Effect + f: (e: Types.NoInfer) => Effect ): Layer> -} = dual(2, ( +} = dual(2, ( self: Layer, - f: (e: XE) => Effect + f: (e: E) => Effect ): Layer> => fromBuild((memoMap, scope) => internalEffect.catch_( self.build(memoMap, scope), - (error) => Scope.provide(internalEffect.andThen(f(error as XE), internalEffect.fail(error)), scope) + (error) => Scope.provide(internalEffect.andThen(f(error), internalEffect.fail(error)), scope) ) )) @@ -1778,22 +1780,24 @@ export const tapError: { * @since 4.0.0 */ export const tapCause: { - ( - f: (cause: Cause.Cause) => Effect + ( + f: (cause: Cause.Cause>) => Effect ): (self: Layer) => Layer> - ( + ( + f: (cause: Cause.Cause) => Effect + ): (self: Layer) => Layer> + ( self: Layer, - f: (cause: Cause.Cause) => Effect + f: (cause: Cause.Cause>) => Effect ): Layer> -} = dual(2, ( +} = dual(2, ( self: Layer, - f: (cause: Cause.Cause) => Effect + f: (cause: Cause.Cause) => Effect ): Layer> => fromBuild((memoMap, scope) => internalEffect.catchCause( self.build(memoMap, scope), - (cause) => - Scope.provide(internalEffect.andThen(f(cause as Cause.Cause), internalEffect.failCause(cause)), scope) + (cause) => Scope.provide(internalEffect.andThen(f(cause), internalEffect.failCause(cause)), scope) ) )) @@ -2676,7 +2680,7 @@ export const withSpan: { (span) => internalEffect.addFinalizer((exit) => options.onEnd!(span, exit)) ) : internalEffect.makeSpanScoped(name, options), - (span) => withParentSpan(self, span) + (span) => withParentSpan(self, span, options) ) ) } @@ -2689,7 +2693,7 @@ export const withSpan: { (span) => internalEffect.addFinalizer((exit) => options.onEnd!(span, exit)) ) : internalEffect.makeSpanScoped(name, options), - (span) => withParentSpan(self, span) + (span) => withParentSpan(self, span, options) ) ) } as any diff --git a/repos/effect/packages/effect/src/LayerMap.ts b/repos/effect/packages/effect/src/LayerMap.ts index eabd4ec59..d9bc31fe0 100644 --- a/repos/effect/packages/effect/src/LayerMap.ts +++ b/repos/effect/packages/effect/src/LayerMap.ts @@ -423,7 +423,7 @@ export const Service = () => : Options extends { readonly layers: infer Layers } ? keyof Layers : never, Service.Success, - Options extends { readonly preload: true } ? never : Service.Error, + Service.Error, Service.Services, Options extends { readonly preload: true } ? Service.Error : Options extends { readonly preloadKeys: Iterable } ? Service.Error @@ -431,11 +431,13 @@ export const Service = () => Options extends { readonly dependencies: ReadonlyArray> } ? Options["dependencies"][number] : never > => { - const Err = globalThis.Error as any const limit = getStackTraceLimit() - setStackTraceLimit(2) - const creationError = new Err() - setStackTraceLimit(limit) + let creationError: Error | undefined + if (limit !== 0) { + setStackTraceLimit(2) + creationError = new globalThis.Error() + setStackTraceLimit(limit) + } function TagClass() {} const TagClass_ = TagClass as any as Mutable> @@ -443,7 +445,7 @@ export const Service = () => TagClass.key = id Object.defineProperty(TagClass, "stack", { get() { - return creationError.stack + return creationError?.stack } }) diff --git a/repos/effect/packages/effect/src/LayerRef.ts b/repos/effect/packages/effect/src/LayerRef.ts index a96100773..4d18aeb85 100644 --- a/repos/effect/packages/effect/src/LayerRef.ts +++ b/repos/effect/packages/effect/src/LayerRef.ts @@ -349,11 +349,13 @@ export const Service = () => [Preload] extends [true] ? E : never, Deps[number] > => { - const Err = globalThis.Error as any const limit = getStackTraceLimit() - setStackTraceLimit(2) - const creationError = new Err() - setStackTraceLimit(limit) + let creationError: Error | undefined + if (limit !== 0) { + setStackTraceLimit(2) + creationError = new globalThis.Error() + setStackTraceLimit(limit) + } function TagClass() {} const TagClass_ = TagClass as any as Mutable> @@ -361,7 +363,7 @@ export const Service = () => TagClass.key = id Object.defineProperty(TagClass, "stack", { get() { - return creationError.stack + return creationError?.stack } }) diff --git a/repos/effect/packages/effect/src/Logger.ts b/repos/effect/packages/effect/src/Logger.ts index 168a53870..2cd9b881a 100644 --- a/repos/effect/packages/effect/src/Logger.ts +++ b/repos/effect/packages/effect/src/Logger.ts @@ -752,7 +752,9 @@ export const batched = dual< /** * A `Logger` which outputs logs in a "pretty" format and writes them to the - * console. + * console. Chooses between tty and browser implementation. If the runtime + * platform is known and fixed, prefer {@link consolePrettyBrowser} or + * {@link consolePrettyTty}. * * **Details** * @@ -763,24 +765,135 @@ export const batched = dual< * **Example** (Logging with pretty console output) * * ```ts import.meta.vitest - * import { Logger } from "effect" + * import { Effect, Logger } from "effect" + * + * const prettyLogger = Logger.layer([Logger.consolePretty()]) + * + * Effect.log("hello").pipe( + * Effect.withLogSpan('label'), + * Effect.annotateLogs('key', 'value'), + * Effect.provide(prettyLogger), + * Effect.runSync + * ) + * ``` + * + * **Example** (Logging with console.error, when the environment has TTY) + * + * ```ts import.meta.vitest + * import { Effect, Layer, Logger } from "effect" * - * const prettyLogger = Logger.consolePretty({ colors: false }) - * Logger.isLogger(prettyLogger) // => true + * const prettyLoggerLayer = Layer.merge( + * Logger.layer([Logger.consolePretty()]), + * Layer.succeed(Logger.LogToStderr, true) + * ) + * + * Effect.log('hello').pipe( + * Effect.provide(prettyLoggerLayer), + * Effect.runSync + * ) * ``` * + * @see {@link consolePrettyBrowser} for browser-specific implementation + * @see {@link consolePrettyTty} for the TTY-mode implementation * @category constructors * @since 4.0.0 */ export const consolePretty: ( options?: { readonly colors?: "auto" | boolean | undefined - readonly stderr?: boolean | undefined readonly formatDate?: ((date: Date) => string) | undefined readonly mode?: "browser" | "tty" | "auto" | undefined } ) => Logger = effect.consolePretty +/** + * A `Logger` which outputs logs in a "pretty" format and writes them to the + * console. Intended to be used on platforms with a browser console. + * + * **Details** + * + * For example, pretty output can render as + * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line + * such as `key: value`. + * + * **Example** (Logging with pretty console output) + * + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * + * const prettyLogger = Logger.layer([Logger.consolePrettyBrowser()]) + * + * Effect.log("hello").pipe( + * Effect.withLogSpan('label'), + * Effect.annotateLogs('key', 'value'), + * Effect.provide(prettyLogger), + * Effect.runSync + * ) + * ``` + * + * @see {@link consolePretty} for the platform-independent implementation + * @category constructors + * @since 4.0.0 + */ +export const consolePrettyBrowser: ( + options?: { + readonly colors?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + } +) => Logger = effect.prettyLoggerBrowser + +/** + * A `Logger` which outputs logs in a "pretty" format and writes them to the + * console. Intended to be used on platforms with tty console. + * + * **Details** + * + * For example, pretty output can render as + * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line + * such as `key: value`. + * + * **Example** (Logging with pretty console output) + * + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * + * const prettyLogger = Logger.layer([Logger.consolePrettyTty()]) + * + * Effect.log("hello").pipe( + * Effect.withLogSpan('label'), + * Effect.annotateLogs('key', 'value'), + * Effect.provide(prettyLogger), + * Effect.runSync + * ) + * ``` + * + * **Example** (Logging with console.error) + * + * ```ts import.meta.vitest + * import { Effect, Layer, Logger } from "effect" + * + * const prettyLoggerLayer = Layer.merge( + * Logger.layer([Logger.consolePrettyTty()]), + * Layer.succeed(Logger.LogToStderr, true) + * ) + * + * Effect.log('hello').pipe( + * Effect.provide(prettyLoggerLayer), + * Effect.runSync + * ) + * ``` + * + * @see {@link consolePretty} for the platform-independent implementation + * @category constructors + * @since 4.0.0 + */ +export const consolePrettyTty: ( + options?: { + readonly colors?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + } +) => Logger = effect.prettyLoggerTty + /** * A `Logger` which outputs logs using the [logfmt](https://brandur.org/logfmt) * style and writes them to the console. @@ -951,9 +1064,8 @@ export const layer = < * * const writes: Array = [] * const file = { - * write: (buffer: Uint8Array) => Effect.sync(() => { + * writeAll: (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) }) @@ -977,9 +1089,8 @@ export const layer = < * * const writes: Array = [] * const file = { - * write: (buffer: Uint8Array) => Effect.sync(() => { + * writeAll: (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) }) @@ -1030,7 +1141,7 @@ export const toFile = dual< const encoder = new TextEncoder() return yield* batched(self, { window: options?.batchWindow ?? 1000, - flush: (output) => effect.ignore(logFile.write(encoder.encode(output.join("\n") + "\n"))) + flush: (output) => effect.ignore(logFile.writeAll(encoder.encode(output.join("\n") + "\n"))) }) }) ) diff --git a/repos/effect/packages/effect/src/ManagedRuntime.ts b/repos/effect/packages/effect/src/ManagedRuntime.ts index 90117459f..e0a99849d 100644 --- a/repos/effect/packages/effect/src/ManagedRuntime.ts +++ b/repos/effect/packages/effect/src/ManagedRuntime.ts @@ -317,7 +317,7 @@ export const make = ( self.cachedContext = context }) ), - { ...defaultRunOptions, scheduler: fiber.currentScheduler } + { ...defaultRunOptions, scheduler: fiber.cache.scheduler } ) } return Effect.flatten(Fiber.await(buildFiber)) diff --git a/repos/effect/packages/effect/src/Match.ts b/repos/effect/packages/effect/src/Match.ts index 918c44d20..1f3a4aaad 100644 --- a/repos/effect/packages/effect/src/Match.ts +++ b/repos/effect/packages/effect/src/Match.ts @@ -22,7 +22,7 @@ const TypeId = internal.TypeId // The conditional must stay deferred until P is inferred. Replacing it with an // intersection loses contextual typing for nested generic calls (microsoft/TypeScript#52864). -type Contextual = internal.Contextual +type Contextual = [P] extends [never] ? Fallback : P type TagHandlers = { readonly [Tag in Types.Tags & string]: (_: Extract>) => Ret diff --git a/repos/effect/packages/effect/src/Metric.ts b/repos/effect/packages/effect/src/Metric.ts index 9a9093c66..14eb02edc 100644 --- a/repos/effect/packages/effect/src/Metric.ts +++ b/repos/effect/packages/effect/src/Metric.ts @@ -1609,7 +1609,15 @@ export const CurrentMetricAttributes = Context.Reference(Cu defaultValue: () => ({}) }) -const MetricRegistryKey = "~effect/Metric/MetricRegistryKey" +const MetricRegistryKey = "effect/Metric/MetricRegistry" + +/** + * The registry used to store metric metadata and hooks. + * + * @category services + * @since 4.0.0 + */ +export type MetricRegistry = Map> /** * Context reference for the metric registry in the current context. @@ -1637,7 +1645,7 @@ const MetricRegistryKey = "~effect/Metric/MetricRegistryKey" * @category services * @since 4.0.0 */ -export const MetricRegistry = Context.Reference>>( +export const MetricRegistry: Context.Reference = Context.Reference( MetricRegistryKey, { defaultValue: () => new Map() } ) @@ -1652,8 +1660,7 @@ abstract class Metric$ implements Metric { declare readonly Input: Contravariant declare readonly State: Covariant - readonly #metadataCache = new WeakMap>() - #metadata: Metric.Metadata | undefined + readonly #metadata = new WeakMap>() readonly id: string readonly description: string | undefined @@ -1685,20 +1692,15 @@ abstract class Metric$ implements Metric { hook(context: Context.Context): Metric.Hooks { const extraAttributes = Context.get(context, CurrentMetricAttributes) - if (Object.keys(extraAttributes).length === 0) { - if (Predicate.isNotUndefined(this.#metadata)) { - return this.#metadata.hooks - } - this.#metadata = this.getOrCreate(context, this.attributes) - return this.#metadata.hooks + if (Object.keys(extraAttributes).length > 0) { + return this.getOrCreate(context, mergeAttributes(this.attributes, extraAttributes)).hooks } - const mergedAttributes = mergeAttributes(this.attributes, extraAttributes) - let metadata = this.#metadataCache.get(mergedAttributes) - if (Predicate.isNotUndefined(metadata)) { - return metadata.hooks + const registry = Context.get(context, MetricRegistry) + let metadata = this.#metadata.get(registry) + if (Predicate.isUndefined(metadata)) { + metadata = this.getOrCreate(context, this.attributes) + this.#metadata.set(registry, metadata) } - metadata = this.getOrCreate(context, mergedAttributes) - this.#metadataCache.set(mergedAttributes, metadata) return metadata.hooks } @@ -3235,14 +3237,13 @@ const fiberFailures = counter("child_fiber_failures", { * ```ts import.meta.vitest * import { Metric } from "effect" * - * Metric.FiberRuntimeMetricsKey // => "effect/observability/Metric/FiberRuntimeMetricsKey" + * Metric.FiberRuntimeMetricsKey // => "effect/Metric/FiberRuntimeMetrics" * ``` * * @category constants * @since 4.0.0 */ -export const FiberRuntimeMetricsKey: "effect/observability/Metric/FiberRuntimeMetricsKey" = - InternalMetric.FiberRuntimeMetricsKey +export const FiberRuntimeMetricsKey: "effect/Metric/FiberRuntimeMetrics" = InternalMetric.FiberRuntimeMetricsKey /** * Interface for the fiber runtime metrics service that tracks fiber lifecycle events. @@ -3496,7 +3497,9 @@ function makeHooks( } function serializeAttributes(attributes: Metric.Attributes): string { - return JSON.stringify(Array.isArray(attributes) ? attributes : Object.entries(attributes)) + const entries = Array.isArray(attributes) ? [...attributes] : Object.entries(attributes) + entries.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0) + return JSON.stringify(entries) } function mergeAttributes( diff --git a/repos/effect/packages/effect/src/MutableList.ts b/repos/effect/packages/effect/src/MutableList.ts index d75f55c39..3506009e8 100644 --- a/repos/effect/packages/effect/src/MutableList.ts +++ b/repos/effect/packages/effect/src/MutableList.ts @@ -291,6 +291,7 @@ export const prependAllUnsafe = (self: MutableList, messages: ReadonlyArra offset: 0, next: self.head } + if (!self.tail && messages.length > 0) self.tail = self.head self.length += self.head.array.length } diff --git a/repos/effect/packages/effect/src/Number.ts b/repos/effect/packages/effect/src/Number.ts index 10772b72c..b42e1c07e 100644 --- a/repos/effect/packages/effect/src/Number.ts +++ b/repos/effect/packages/effect/src/Number.ts @@ -643,7 +643,7 @@ export const remainder: { const selfDecCount = (selfString.split(".")[1] || "").length const divisorDecCount = (divisorString.split(".")[1] || "").length const decCount = selfDecCount > divisorDecCount ? selfDecCount : divisorDecCount - const selfInt = parseInt(self.toFixed(decCount).replace(".", "")) + const selfInt = self === 0 ? self : parseInt(self.toFixed(decCount).replace(".", "")) const divisorInt = parseInt(divisor.toFixed(decCount).replace(".", "")) return (selfInt % divisorInt) / Math.pow(10, decCount) }) diff --git a/repos/effect/packages/effect/src/Optic.ts b/repos/effect/packages/effect/src/Optic.ts index f132e6617..c21149a6f 100644 --- a/repos/effect/packages/effect/src/Optic.ts +++ b/repos/effect/packages/effect/src/Optic.ts @@ -12,6 +12,7 @@ * @since 4.0.0 */ +import * as Arr from "./Array.ts" import { dual, identity } from "./Function.ts" import * as InternalRecord from "./internal/record.ts" import * as Option from "./Option.ts" @@ -1001,8 +1002,11 @@ class OptionalImpl implements Optional { (a, s) => { const copy = cloneShallow(s) if (a === undefined) { - if (Array.isArray(copy) && typeof key === "number") { - copy.splice(key, 1) + if ( + Array.isArray(copy) && + (typeof key === "number" || (typeof key === "string" && Arr.isCanonicalArrayIndex(key))) + ) { + copy.splice(Number(key), 1) } else { delete copy[key] } @@ -1058,10 +1062,10 @@ class OptionalImpl implements Optional { ) } pick(keys: any) { - return this.compose(makeLens(Struct.pick(keys), (p, a) => ({ ...a, ...p }))) + return this.compose(makeLens(Struct.pick(keys), (p, a) => ({ ...Struct.omit(a, keys), ...p }))) } omit(keys: any) { - return this.compose(makeLens(Struct.omit(keys), (o, a) => ({ ...a, ...o }))) + return this.compose(makeLens(Struct.omit(keys), (o, a) => ({ ...Struct.pick(a, keys), ...o }))) } notUndefined(): any { return this.refine(Predicate.isNotUndefined, { expected: "a value other than `undefined`" }) diff --git a/repos/effect/packages/effect/src/Order.ts b/repos/effect/packages/effect/src/Order.ts index c52ea3604..211b44bbd 100644 --- a/repos/effect/packages/effect/src/Order.ts +++ b/repos/effect/packages/effect/src/Order.ts @@ -368,6 +368,7 @@ export function alwaysEqual(): Order { * * Applies orders in iteration order and short-circuits on the first non-zero * result. It returns `0` only if all orders return `0`. + * The collection is materialized when the order is created, so it must be finite. * * **Example** (Combining multiple Orders) * @@ -397,9 +398,10 @@ export function alwaysEqual(): Order { * @since 2.0.0 */ export function combineAll(collection: Iterable>): Order { + const orders = Array.from(collection) return make((a1, a2) => { let out: Ordering = 0 - for (const O of collection) { + for (const O of orders) { out = O(a1, a2) if (out !== 0) { return out diff --git a/repos/effect/packages/effect/src/PartitionedSemaphore.ts b/repos/effect/packages/effect/src/PartitionedSemaphore.ts index d700e46ee..07aaabfb6 100644 --- a/repos/effect/packages/effect/src/PartitionedSemaphore.ts +++ b/repos/effect/packages/effect/src/PartitionedSemaphore.ts @@ -216,8 +216,7 @@ export const makeUnsafe = (options: { } const cleanup = () => { - waiters.delete(entry) - if (waiters.size === 0) { + if (waiters.delete(entry) && waiters.size === 0) { MutableHashMap.remove(partitions, key) } } diff --git a/repos/effect/packages/effect/src/Pool.ts b/repos/effect/packages/effect/src/Pool.ts index b160722d3..cdb5ee17c 100644 --- a/repos/effect/packages/effect/src/Pool.ts +++ b/repos/effect/packages/effect/src/Pool.ts @@ -119,6 +119,7 @@ export interface State { isShuttingDown: boolean usage: number readonly resizeSemaphore: Semaphore.Semaphore + // Insertion order determines usage-TTL retirement order; reclaimed items move to the back. readonly items: Set> availableHead: PoolItem | undefined availableTail: PoolItem | undefined @@ -673,9 +674,12 @@ const wakeAll = (self: Pool): Effect.Effect => return internal.void }) +// Reservations prevent reuse without extending the lifetime of borrowed items. +const reservations = new WeakMap, number>() + /** Adds a freshly acquired item, which has no use behind it, at the back. */ const addAvailable = (self: Pool, item: PoolItem): void => { - if (item.isAvailable) return + if (item.isAvailable || reservations.has(item)) return item.isAvailable = true item.availablePrevious = self.state.availableTail item.availableNext = undefined @@ -701,7 +705,7 @@ const addAvailable = (self: Pool, item: PoolItem): void => { * item is checked out either way. */ const addAvailableFront = (self: Pool, item: PoolItem): void => { - if (item.isAvailable) return + if (item.isAvailable || reservations.has(item)) return item.isAvailable = true item.availablePrevious = undefined item.availableNext = self.state.availableHead @@ -783,9 +787,12 @@ export const reserve: { (self: Pool, item: A): Effect.Effect => Effect.asVoid(Effect.acquireRelease( Effect.sync(() => { + if (self.config.concurrency === 1) return undefined for (const poolItem of self.state.items) { if (poolItem.exit._tag !== "Success" || poolItem.exit.value !== item) continue - self.state.usage += self.config.concurrency - 1 + const existing = reservations.get(poolItem) + if (existing === undefined) self.state.usage += self.config.concurrency - 1 + reservations.set(poolItem, (existing ?? 0) + 1) removeAvailable(self, poolItem) return poolItem } @@ -794,15 +801,22 @@ export const reserve: { (poolItem) => core.withFiber((fiber) => { if (poolItem === undefined) return internal.void + const remaining = (reservations.get(poolItem) ?? 1) - 1 + if (remaining > 0) { + reservations.set(poolItem, remaining) + return internal.void + } + reservations.delete(poolItem) self.state.usage -= self.config.concurrency - 1 if ( + !self.state.isShuttingDown && self.state.items.has(poolItem) && !self.state.invalidated.has(poolItem) && poolItem.refCount < self.config.concurrency ) { - addAvailable(self, poolItem) + addAvailableFront(self, poolItem) + wakeWaiters(self, fiber, self.config.concurrency - poolItem.refCount) } - wakeWaiters(self, fiber, self.config.concurrency - 1) return internal.void }) )) @@ -891,10 +905,12 @@ const allocate = (self: Pool): Effect.Effect> => if (self.config.strategy === strategyNoop) { return exit._tag === "Success" ? Effect.succeed(item) : Effect.as(item.finalizer, item) } + const onAcquire = Effect.suspend(() => + // A borrower may have removed the item before the callback runs. + self.state.items.has(item) ? self.config.strategy.onAcquire(item) : Effect.void + ) return Effect.as( - exit._tag === "Success" - ? self.config.strategy.onAcquire(item) - : Effect.flatMap(item.finalizer, () => self.config.strategy.onAcquire(item)), + exit._tag === "Success" ? onAcquire : Effect.flatMap(item.finalizer, () => onAcquire), item ) }) @@ -964,44 +980,48 @@ const strategyCreationTTL = Effect.fnUntraced(function*(ttl: Duration.Inpu }) }) -const strategyUsageTTL = Effect.fnUntraced(function*(ttl: Duration.Input) { - const queue = yield* Queue.unbounded>() - return identity>({ +const strategyUsageTTL = (ttl: Duration.Input): Effect.Effect> => + Effect.succeed>({ run: (pool) => { + // `state.items` iterates in insertion order, so its oldest live entry is + // the next to retire. Using it directly means an item stops being + // referenced by this strategy as soon as it leaves the pool. const process: Effect.Effect = Effect.suspend(() => { - const excess = activeSize(pool) - targetSize(pool) - if (excess <= 0) return Effect.void - return Queue.take(queue).pipe( - Effect.tap((item) => invalidatePoolItem(pool, item)), - Effect.flatMap(() => process) - ) + if (activeSize(pool) <= targetSize(pool)) return Effect.void + for (const item of pool.state.items) { + if (pool.state.invalidated.has(item)) continue + return Effect.flatMap(invalidatePoolItem(pool, item), () => process) + } + return Effect.void }) return process.pipe( Effect.delay(ttl), Effect.forever({ disableYield: true }) ) }, - onAcquire: (item) => Queue.offer(queue, item), + onAcquire: (_) => Effect.void, reclaim(pool) { return Effect.suspend((): Effect.Effect | undefined> => { if (pool.state.invalidated.size === 0) { return Effect.undefined } const item = Iterable.head( - Iterable.filter(pool.state.invalidated, (item) => !item.disableReclaim) + Iterable.filter(pool.state.invalidated, (item) => !item.disableReclaim && !reservations.has(item)) ) if (item._tag === "None") { return Effect.undefined } pool.state.invalidated.delete(item.value) + // Re-adding moves the reclaimed item to the back of the retirement order. + pool.state.items.delete(item.value) + pool.state.items.add(item.value) if (item.value.refCount < pool.config.concurrency) { addAvailable(pool, item.value) } - return Effect.as(Queue.offer(queue, item.value), item.value) + return Effect.succeed(item.value) }) } }) -}) const reportUnhandledError = (cause: Cause.Cause) => Effect.withFiber((fiber) => { diff --git a/repos/effect/packages/effect/src/PubSub.ts b/repos/effect/packages/effect/src/PubSub.ts index b8e978487..6861798fd 100644 --- a/repos/effect/packages/effect/src/PubSub.ts +++ b/repos/effect/packages/effect/src/PubSub.ts @@ -2020,29 +2020,22 @@ class BoundedPubSubSingleSubscription implements PubSub.BackingSubscri if (this.self.subscribers === 0) { this.self.value = AbsentValue as unknown as A } - this.subscriberIndex += 1 + this.subscriberIndex = this.self.publisherIndex return elem } pollUpTo(n: number): Array { - n = Count.normalize(n) - if (this.isEmpty() || n < 1) { + if (Count.normalize(n) < 1 || this.isEmpty()) { return [] } - const a = this.self.value - this.self.subscribers -= 1 - if (this.self.subscribers === 0) { - this.self.value = AbsentValue as unknown as A - } - this.subscriberIndex += 1 - return [a] + return [this.poll() as A] } unsubscribe(): void { if (!this.unsubscribed) { this.unsubscribed = true this.self.subscriberCount -= 1 - if (this.subscriberIndex !== this.self.publisherIndex) { + if (this.self.subscribers !== 0 && this.subscriberIndex !== this.self.publisherIndex) { this.self.subscribers -= 1 if (this.self.subscribers === 0) { this.self.value = AbsentValue as unknown as A diff --git a/repos/effect/packages/effect/src/Queue.ts b/repos/effect/packages/effect/src/Queue.ts index 43e61266b..6548a4185 100644 --- a/repos/effect/packages/effect/src/Queue.ts +++ b/repos/effect/packages/effect/src/Queue.ts @@ -1106,8 +1106,8 @@ export const interrupt = (self: Enqueue): Effect => * * **Details** * - * The operation is idempotent and returns `true`, including when the queue has - * already been shut down or completed. + * Returns `true` when the queue is shut down by this call, or `false` when it + * has already been shut down or completed. * * **Example** (Shutting down queues) * @@ -1132,29 +1132,49 @@ export const interrupt = (self: Enqueue): Effect => * await Effect.runPromise(program) // => { wasShutdown: true, size: 0 } * ``` * + * @see {@link shutdownUnsafe} for synchronous shutdown * @category completion * @since 2.0.0 */ -export const shutdown = (self: Enqueue): Effect => - internalEffect.sync(() => { - if (self.state._tag === "Done") { - return true - } - MutableList.clear(self.messages) - const offers = self.state.offers - finalize(self, self.state._tag === "Open" ? exitInterrupt : self.state.exit) - if (offers.size > 0) { - for (const entry of offers) { - if (entry._tag === "Single") { - entry.resume(exitFalse) - } else { - entry.resume(core.exitSucceed(entry.remaining.slice(entry.offset))) - } - } - offers.clear() +export const shutdown = (self: Enqueue): Effect => internalEffect.sync(() => shutdownUnsafe(self)) + +/** + * Shuts down the queue synchronously, discarding buffered messages and resuming + * pending operations. + * + * **When to use** + * + * Use when a synchronous callback must discard buffered messages and settle + * pending queue operations before returning. + * + * **Details** + * + * An open queue completes with an interruption. A queue already closing retains + * its completion cause. Call `failCauseUnsafe` first to shut down with a specific + * failure. Returns `true` when the queue is shut down by this call, or `false` + * when it has already been shut down or completed. + * + * @see {@link shutdown} for the effectful variant + * @see {@link failCauseUnsafe} to set a failure before discarding buffered messages + * @category completion + * @since 4.0.0 + */ +export const shutdownUnsafe = (self: Enqueue): boolean => { + if (self.state._tag === "Done") { + return false + } + MutableList.clear(self.messages) + const offers = self.state.offers + finalize(self, self.state._tag === "Open" ? exitInterrupt : self.state.exit) + for (const entry of offers) { + if (entry._tag === "Single") { + entry.resume(exitFalse) + } else { + entry.resume(core.exitSucceed(entry.remaining.slice(entry.offset))) } - return true - }) + } + return true +} /** * Takes and returns all currently buffered messages without waiting for more. @@ -1564,16 +1584,89 @@ export const takeUnsafe = (self: Dequeue): Exit | undefined => releaseCapacity(self) return core.exitSucceed(message) } else if (self.capacity <= 0 && self.state.offers.size > 0) { - self.capacity = 1 - releaseCapacity(self) - self.capacity = 0 - const message = MutableList.take(self.messages)! + const message = takeOfferUnsafe(self.state.offers) releaseCapacity(self) return core.exitSucceed(message) } return undefined } +/** + * Manually releases current queue takers synchronously. + * + * **When to use** + * + * Use when synchronous offers should release waiting consumers immediately + * instead of waiting for the scheduled release task. + * + * **Details** + * + * This immediately runs the queue's taker-release pass instead of waiting for + * its scheduled task. It does not complete the queue or resume fibers waiting + * on `Queue.await`. + * + * **Example** (Releasing a waiting taker synchronously) + * + * ```ts import.meta.vitest + * import { Effect, Fiber, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.unbounded() + * const taker = yield* Queue.take(queue).pipe(Effect.forkChild) + * yield* Effect.yieldNow + * + * Queue.offerUnsafe(queue, 1) + * Queue.flushUnsafe(queue) + * + * return yield* Fiber.join(taker) + * }) + * + * await Effect.runPromise(program) // => 1 + * ``` + * + * @category offering + * @since 4.0.0 + */ +export const flushUnsafe = (self: Enqueue): void => releaseTakers(self) + +/** + * Manually releases current queue takers. + * + * **When to use** + * + * Use when synchronous offers should release waiting consumers through an + * `Effect` instead of waiting for the scheduled release task. + * + * **Details** + * + * This immediately runs the queue's taker-release pass instead of waiting for + * its scheduled task. It does not complete the queue or resume fibers waiting + * on `Queue.await`. + * + * **Example** (Releasing a waiting taker) + * + * ```ts import.meta.vitest + * import { Effect, Fiber, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.unbounded() + * const taker = yield* Queue.take(queue).pipe(Effect.forkChild) + * yield* Effect.yieldNow + * + * Queue.offerUnsafe(queue, 1) + * yield* Queue.flush(queue) + * + * return yield* Fiber.join(taker) + * }) + * + * await Effect.runPromise(program) // => 1 + * ``` + * + * @category offering + * @since 4.0.0 + */ +export const flush = (self: Enqueue): Effect => internalEffect.sync(() => flushUnsafe(self)) + const await_ = (self: Dequeue): Effect> => internalEffect.callback>((resume) => { const awaiter = (effect: Effect) => resume(Pull.catchDone(effect, () => internalEffect.exitVoid)) @@ -1842,7 +1935,6 @@ const exitFailDone = core.exitFail(core.Done()) as Failure const exitInterrupt = internalEffect.exitInterrupt() as Failure const releaseTakers = (self: Enqueue) => { - self.scheduleRunning = false if (self.state._tag === "Done" || self.state.takers.size === 0) { return } @@ -1860,7 +1952,10 @@ const scheduleReleaseTaker = (self: Enqueue) => { return } self.scheduleRunning = true - self.dispatcher.scheduleTask(() => releaseTakers(self), 0) + self.dispatcher.scheduleTask(() => { + self.scheduleRunning = false + releaseTakers(self) + }, 0) } const takeBetweenUnsafe = ( @@ -1872,11 +1967,8 @@ const takeBetweenUnsafe = ( return self.state.exit } else if (max <= 0 || min <= 0) { return core.exitSucceed([]) - } else if (self.capacity <= 0 && self.state.offers.size > 0) { - self.capacity = 1 - releaseCapacity(self) - self.capacity = 0 - const messages = [MutableList.take(self.messages)!] + } else if (self.capacity <= 0 && self.messages.length === 0 && self.state.offers.size > 0) { + const messages = [takeOfferUnsafe(self.state.offers)] releaseCapacity(self) return core.exitSucceed(messages) } @@ -1923,6 +2015,22 @@ const offerRemainingArray = (self: Enqueue, remaining: Array) => }) } +// Reserve a pending message for the consumer before the producer can reenter. +const takeOfferUnsafe = (offers: Set>): A => { + const entry = offers.values().next().value! + if (entry._tag === "Single") { + offers.delete(entry) + entry.resume(exitTrue) + return entry.message + } + const message = entry.remaining[entry.offset++] + if (entry.offset === entry.remaining.length) { + offers.delete(entry) + entry.resume(core.exitSucceed([])) + } + return message +} + const releaseCapacity = (self: Dequeue): boolean => { if (self.state._tag === "Done") { return Pull.isDoneCause(self.state.exit.cause) @@ -1936,22 +2044,22 @@ const releaseCapacity = (self: Dequeue): boolean => { } return false } - let n = self.capacity - self.messages.length + // Resuming a producer can synchronously take, offer, or shut down this queue. for (const entry of self.state.offers) { - if (n === 0) break + let n = self.capacity - self.messages.length + if (n <= 0) break else if (entry._tag === "Single") { MutableList.append(self.messages, entry.message) - n-- - entry.resume(exitTrue) self.state.offers.delete(entry) + entry.resume(exitTrue) } else { for (; entry.offset < entry.remaining.length; entry.offset++) { if (n === 0) return false MutableList.append(self.messages, entry.remaining[entry.offset]) n-- } - entry.resume(core.exitSucceed([])) self.state.offers.delete(entry) + entry.resume(core.exitSucceed([])) } } return false @@ -1976,10 +2084,7 @@ const takeAllUnsafe = (self: Dequeue) => { releaseCapacity(self) return messages } else if (self.state._tag !== "Done" && self.state.offers.size > 0) { - self.capacity = 1 - releaseCapacity(self) - self.capacity = 0 - const messages = [MutableList.take(self.messages)!] + const messages = [takeOfferUnsafe(self.state.offers)] releaseCapacity(self) return messages } diff --git a/repos/effect/packages/effect/src/Random.ts b/repos/effect/packages/effect/src/Random.ts index 2afe37ea1..67b2c5781 100644 --- a/repos/effect/packages/effect/src/Random.ts +++ b/repos/effect/packages/effect/src/Random.ts @@ -17,6 +17,23 @@ import * as random from "./internal/random.ts" import type * as NonEmptyIterable from "./NonEmptyIterable.ts" import * as Predicate from "./Predicate.ts" +/** + * The service used to generate pseudo-random numbers. + * + * @category services + * @since 4.0.0 + */ +export interface Random { + /** + * Generates a random safe integer. + */ + nextIntUnsafe(): number + /** + * Generates a random number between 0 (inclusive) and 1 (exclusive). + */ + nextDoubleUnsafe(): number +} + /** * Represents a service for generating pseudo-random numbers. * @@ -49,12 +66,9 @@ import * as Predicate from "./Predicate.ts" * @category services * @since 2.0.0 */ -export const Random: Context.Reference<{ - nextIntUnsafe(): number - nextDoubleUnsafe(): number -}> = random.Random +export const Random: Context.Reference = random.Random -const randomWith = (f: (random: typeof Random["Service"]) => A): Effect.Effect => +const randomWith = (f: (random: Random) => A): Effect.Effect => Effect.withFiber((fiber) => Effect.succeed(f(fiber.getRef(Random)))) /** @@ -139,7 +153,7 @@ export const nextInt: Effect.Effect = randomWith((r) => r.nextIntUnsafe( * @since 4.0.0 */ export const nextBetween = (min: number, max: number): Effect.Effect => - randomWith((r) => r.nextDoubleUnsafe() * (max - min) + min) + randomWith((r) => random.nextBetween(min, max, r.nextDoubleUnsafe())) /** * Generates a random integer between `min` and `max`. diff --git a/repos/effect/packages/effect/src/RcMap.ts b/repos/effect/packages/effect/src/RcMap.ts index a99798939..e484c4fd2 100644 --- a/repos/effect/packages/effect/src/RcMap.ts +++ b/repos/effect/packages/effect/src/RcMap.ts @@ -370,7 +370,7 @@ export const get: { context.set(key, value) }) context.set(Scope.Scope.key, entry.scope) - self.lookup(key).pipe( + Effect.suspend(() => self.lookup(key)).pipe( Effect.runForkWith(Context.makeUnsafe(context)), Fiber.runIn(entry.scope) ).addObserver((exit) => Deferred.doneUnsafe(entry.deferred, exit)) diff --git a/repos/effect/packages/effect/src/RcRef.ts b/repos/effect/packages/effect/src/RcRef.ts index 3d64d5890..8c81fce74 100644 --- a/repos/effect/packages/effect/src/RcRef.ts +++ b/repos/effect/packages/effect/src/RcRef.ts @@ -158,8 +158,16 @@ export const make: ( options: { readonly acquire: Effect.Effect /** - * When the reference count reaches zero, the resource will be released - * after this duration. + * How long to keep an idle resource after its last reference is released. + * + * If the resource has not been invalidated, finite durations, including `0`, + * schedule release in a forked fiber that the scope releasing the last + * reference does not await. An infinite duration keeps the idle resource + * until invalidation or the RcRef's scope closes. + * + * If this option is omitted or the resource has been invalidated with + * `RcRef.invalidate`, the scope releasing the last reference releases the + * resource and awaits completion. */ readonly idleTimeToLive?: Duration.Input | undefined } diff --git a/repos/effect/packages/effect/src/RequestResolver.ts b/repos/effect/packages/effect/src/RequestResolver.ts index 10ed8edef..951c5f715 100644 --- a/repos/effect/packages/effect/src/RequestResolver.ts +++ b/repos/effect/packages/effect/src/RequestResolver.ts @@ -17,7 +17,7 @@ import type * as Duration from "./Duration.ts" import * as Effect from "./Effect.ts" import * as Exit from "./Exit.ts" import { constTrue, dual, identity } from "./Function.ts" -import { exitFail, exitSucceed } from "./internal/core.ts" +import { exitSucceed } from "./internal/core.ts" import * as Count from "./internal/count.ts" import * as effect from "./internal/effect.ts" import * as internal from "./internal/request.ts" @@ -529,17 +529,18 @@ export const fromEffectTagged = - Effect.matchCause((fns[tag] as any)(requests) as Effect.Effect, unknown, unknown>, { + Effect.matchCause((fns[tag] as any)(requests) as Effect.Effect, unknown, unknown>, { onFailure: (cause) => { for (let i = 0; i < requests.length; i++) { const entry = requests[i] - entry.completeUnsafe(exitFail(cause) as any) + entry.completeUnsafe(Exit.failCause(cause) as any) } }, onSuccess: (res) => { - for (let i = 0; i < res.length; i++) { - const entry = requests[i] - entry.completeUnsafe(exitSucceed(res[i]) as any) + let i = 0 + for (const result of res) { + const entry = requests[i++] + entry.completeUnsafe(exitSucceed(result) as any) } } }), @@ -1118,8 +1119,9 @@ export const asCache: { * * **Gotchas** * - * Entries do not expire by time, and completed failures are cached the same as - * successes. Request equality controls cache hits. + * Entries do not expire by time, and completed failures without interruptions + * are cached the same as successes. Results containing interruptions are not + * cached. Request equality controls cache hits. * * @see {@link asCache} for exposing the resolver as a `Cache` with time-to-live and service lookup controls * @see {@link persisted} for backing persistable requests with the configured persistence store @@ -1167,7 +1169,16 @@ export const withCache: { MutableHashMap.set(cache, entry.request, cached) const prevComplete = entry.completeUnsafe entry.completeUnsafe = function(exit) { - cached.exit = exit as any + if (Exit.hasInterrupts(exit)) { + if (cached.exit === undefined) { + const current = MutableHashMap.get(cache, entry.request) + if (current._tag === "Some" && current.value === cached) { + MutableHashMap.remove(cache, entry.request) + } + } + } else { + cached.exit = exit as any + } prevComplete(exit) } return true @@ -1265,6 +1276,7 @@ export const persisted: { >) const leftover: Array> = [] const toPersist = new Map>() + const completed = new Set>() for (let i = 0; i < results.length; i++) { const entry = entries[i] const exit = results[i] @@ -1274,6 +1286,7 @@ export const persisted: { ) { const prevComplete = entry.completeUnsafe entry.completeUnsafe = function(exit) { + completed.add(entry) toPersist.set(entry.request, exit as any) prevComplete(exit) } @@ -1285,10 +1298,10 @@ export const persisted: { if (!Arr.isArrayNonEmpty(leftover)) { return } - yield* Effect.catchCause(self.runAll(leftover, key), (cause) => { + yield* Effect.catchCause(Effect.suspend(() => self.runAll(leftover, key)), (cause) => { for (let i = 0; i < leftover.length; i++) { const entry = leftover[i] - if (!toPersist.has(entry.request)) continue + if (completed.has(entry)) continue entry.completeUnsafe(Exit.failCause(cause) as any) } return Effect.void diff --git a/repos/effect/packages/effect/src/Scheduler.ts b/repos/effect/packages/effect/src/Scheduler.ts index 1a6b7f87d..a60495eb0 100644 --- a/repos/effect/packages/effect/src/Scheduler.ts +++ b/repos/effect/packages/effect/src/Scheduler.ts @@ -80,25 +80,35 @@ export const Scheduler: Context.Reference = Context.Reference new MixedScheduler() }) -const setImmediate = "setImmediate" in globalThis - ? (f: () => void) => { +const setMicrotask = (f: () => void) => { + let cancelled = false + Promise.resolve().then(() => { + if (!cancelled) f() + }) + return (): void => { + cancelled = true + } +} + +const setTimer: (f: () => void) => () => void = "setImmediate" in globalThis + ? (f) => { // @ts-ignore const timer = globalThis.setImmediate(f) // @ts-ignore return (): void => globalThis.clearImmediate(timer) } - : (f: () => void) => { + : (f) => { const timer = setTimeout(f, 0) return (): void => clearTimeout(timer) } -const setMicrotask = (f: () => void) => { - let cancelled = false - Promise.resolve().then(() => { - if (!cancelled) f() - }) - return (): void => { - cancelled = true +// Some runtimes (e.g. Cloudflare Workers) throw when a timer is set in global +// scope. Fall back to a microtask so effects can still yield at module load. +const setImmediate = (f: () => void) => { + try { + return setTimer(f) + } catch { + return setMicrotask(f) } } @@ -172,7 +182,7 @@ export class MixedScheduler implements Scheduler { * @since 2.0.0 */ shouldYield(fiber: Fiber.Fiber) { - return fiber.currentOpCount >= fiber.maxOpsBeforeYield + return fiber.currentOpCount >= fiber.cache.maxOpsBeforeYield } /** diff --git a/repos/effect/packages/effect/src/Schema.ts b/repos/effect/packages/effect/src/Schema.ts index 1073798fe..8cedb180b 100644 --- a/repos/effect/packages/effect/src/Schema.ts +++ b/repos/effect/packages/effect/src/Schema.ts @@ -12,7 +12,6 @@ * @since 4.0.0 */ -/** @effect-diagnostics schemaStructWithTag:skip-file */ import * as Arr from "./Array.ts" import * as BigDecimal_ from "./BigDecimal.ts" import type * as Brand from "./Brand.ts" @@ -30,8 +29,8 @@ import type * as Equivalence from "./Equivalence.ts" import * as Exit_ from "./Exit.ts" import type { Formatter } from "./Formatter.ts" import { format, formatPropertyKey } from "./Formatter.ts" -import { identity, memoize } from "./Function.ts" -import * as Graph_ from "./Graph.ts" +import { identity } from "./Function.ts" +import type * as Graph_ from "./Graph.ts" import * as HashMap_ from "./HashMap.ts" import * as HashSet_ from "./HashSet.ts" import * as core from "./internal/core.ts" @@ -39,15 +38,22 @@ import { effectIsExit } from "./internal/effect.ts" import * as InternalGraph from "./internal/graph.ts" import * as InternalRecord from "./internal/record.ts" import * as InternalAnnotations from "./internal/schema/annotations.ts" -import * as InternalSchema from "./internal/schema/schema.ts" +import * as InternalMake from "./internal/schema/make.ts" +import * as InternalStandardSchema from "./internal/schema/standardSchema.ts" +import * as InternalToCodec from "./internal/schema/toCodec.ts" +import * as InternalToDifferJsonPatch from "./internal/schema/toDifferJsonPatch.ts" +import * as InternalToEncoderXml from "./internal/schema/toEncoderXml.ts" import * as InternalEquivalence from "./internal/schema/toEquivalence.ts" +import * as InternalToFormatter from "./internal/schema/toFormatter.ts" +import * as InternalToIso from "./internal/schema/toIso.ts" import * as InternalToJsonSchemaDocument from "./internal/schema/toJsonSchemaDocument.ts" import * as InternalToRepresentation from "./internal/schema/toRepresentation.ts" +import { isSchemaError as isSchemaErrorInternal, SchemaErrorTypeId } from "./internal/schemaError.ts" import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" -import * as JsonPatch from "./JsonPatch.ts" -import * as JsonSchema from "./JsonSchema.ts" +import type * as JsonPatch from "./JsonPatch.ts" +import type * as JsonSchema from "./JsonSchema.ts" import { remainder } from "./Number.ts" -import * as Optic_ from "./Optic.ts" +import type * as Optic_ from "./Optic.ts" import * as Option_ from "./Option.ts" import * as Order from "./Order.ts" import * as Pipeable from "./Pipeable.ts" @@ -56,7 +62,6 @@ 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 * as SchemaGetter from "./SchemaGetter.ts" import * as SchemaIssue from "./SchemaIssue.ts" @@ -68,9 +73,14 @@ import type { Assign, Lambda, Mutable, Simplify } from "./Struct.ts" import * as Struct_ from "./Struct.ts" import type { RequiredKeys, UnionToIntersection } from "./Types.ts" import type { Unify } from "./Unify.ts" +import * as Cookies_ from "./unstable/http/Cookies.ts" +import * as Headers_ from "./unstable/http/Headers.ts" +import * as UrlParams_ from "./unstable/http/UrlParams.ts" +import * as IpInterface_ from "./unstable/net/IpInterface.ts" +import * as IpNetwork_ from "./unstable/net/IpNetwork.ts" +import * as NetAddress_ from "./unstable/net/NetAddress.ts" -const TypeId = InternalSchema.TypeId - +const TypeId = InternalMake.TypeId /** * Whether a schema field is required or optional within a struct. * @@ -81,7 +91,6 @@ const TypeId = InternalSchema.TypeId * @since 4.0.0 */ export type Optionality = "required" | "optional" - /** * Whether a schema field is readonly or mutable within a struct. * @@ -91,7 +100,6 @@ export type Optionality = "required" | "optional" * @since 4.0.0 */ export type Mutability = "readonly" | "mutable" - /** * Whether a schema field has a constructor default value. * @@ -102,14 +110,13 @@ export type Mutability = "readonly" | "mutable" * @since 4.0.0 */ export type ConstructorDefault = "no-default" | "with-default" - /** * Options for `makeEffect`, `make`, and Class constructors. * * **When to use** * - * Use when passing `disableChecks: true` to skip validation when you trust the data. - * - Pass `parseOptions` to control error reporting behavior. + * Use when passing `disableChecks: true` to skip validation when you trust the + * data. Pass `parseOptions` to control parsing behavior. * * @see {@link BottomWithoutNew.makeEffect} * @see {@link BottomWithoutNew.make} @@ -133,7 +140,6 @@ export interface MakeOptions { readonly value: unknown } } - /** * The fully-parameterized schema interface without a construct signature. * Exposes all 14 type parameters controlling type inference, mutability, @@ -165,26 +171,21 @@ export interface BottomWithoutNew< out EncodedOptionality extends Optionality = "required" > extends Pipeable.Pipeable { readonly [TypeId]: typeof TypeId - readonly "ast": Ast readonly "Rebuild": Rebuild readonly "~type.parameters": TypeParameters - readonly "Type": T readonly "Encoded": E readonly "DecodingServices": RD readonly "EncodingServices": RE - readonly "~type.make.in": TypeMakeIn - readonly "~type.make": TypeMake // useful to type the `refine` interface + readonly "~type.make": TypeMake readonly "~type.constructor.default": TypeConstructorDefault readonly "Iso": Iso - readonly "~type.mutability": TypeMutability readonly "~type.optionality": TypeOptionality readonly "~encoded.mutability": EncodedMutability readonly "~encoded.optionality": EncodedOptionality - annotate(annotations: Annotations.Bottom): this["Rebuild"] annotateKey(annotations: Annotations.Key): this["Rebuild"] check(...checks: readonly [SchemaAST.Check, ...Array>]): this["Rebuild"] @@ -245,20 +246,29 @@ export interface BottomWithoutNew< * * **When to use** * - * Use when constructor input may fail validation and you want to - * compose that failure with other `Effect` operations instead of throwing. + * Use when construction must compose with other `Effect` operations or when + * the result supplies a nested constructor default. Otherwise, use + * {@link BottomWithoutNew.make} when invalid construction should throw. * * **Details** * - * Validation failures are returned directly as `SchemaIssue.Issue` values - * and are not wrapped in `SchemaError`. + * `SchemaParser` uses `SchemaIssue.Issue` as its canonical structured failure, + * and this method exposes the same representation directly. Keeping the issue + * unwrapped lets an enclosing schema attach its own path when the effect is + * used with {@link withConstructorDefault}. It also avoids allocating a + * `SchemaError` and keeps its formatting path out of the resulting bundle. + * + * **Gotchas** + * + * Unlike the decoding helpers in `Schema`, validation failures are not wrapped + * in `SchemaError`. Map the issue to `SchemaError` or to a domain-specific error + * when a wrapped error is needed at the application boundary. * * @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 } - /** * Fully-parameterized base interface for schemas that can be extended directly * by TypeScript classes. @@ -316,7 +326,6 @@ export interface Bottom< { new(_: never): {} } - /** * Lazy `BottomWithoutNew` variant for schema implementations that * compute their public views on demand. @@ -369,7 +378,6 @@ export interface BottomLazyWithoutNew< EncodedOptionality > {} - /** * Lazy `Bottom` variant for schemas that can be extended directly by TypeScript * classes. @@ -413,7 +421,6 @@ export interface BottomLazy< { new(_: never): {} } - /** * Type-level representation returned by {@link declareConstructor}. * @@ -433,7 +440,6 @@ export interface declareConstructor {} - /** * Creates a schema for a **parametric** type (a generic container such as * `Array`, `Option`, etc.) by accepting a list of type-parameter schemas @@ -512,7 +518,6 @@ export function declareConstructor() { ) } } - /** * Type-level representation returned by {@link declare}. * @@ -522,7 +527,6 @@ export function declareConstructor() { export interface declare extends declareConstructor { readonly "Rebuild": declare } - /** * Creates a schema for a **non-parametric** opaque type using a type-guard * function. The schema accepts any unknown value and succeeds when `is` returns @@ -568,7 +572,6 @@ export function declare( annotations ) } - /** * Returns a schema widened to the fully-parameterized {@link Bottom} interface, * making all 14 type parameters visible to TypeScript. @@ -619,7 +622,6 @@ export function revealBottom( > { return bottom } - /** * Adds metadata annotations to a schema without changing its runtime behavior. * This is the pipeable (curried) counterpart of the `.annotate` method. @@ -652,7 +654,6 @@ export function revealBottom( export function annotate(annotations: Annotations.Bottom) { return (self: S) => self.annotate(annotations) } - /** * Adds metadata annotations to the **encoded** side of a schema without * changing its runtime behavior. This is the encoded-side counterpart of @@ -685,7 +686,6 @@ export function annotate(annotations: Annotations.Bottom(annotations: Annotations.Bottom) { return (self: S): S["Rebuild"] => flip(flip(self).annotate(annotations)) } - /** * Adds key-level annotations to a schema field. This is the pipeable * (curried) counterpart of the `.annotateKey` method. @@ -722,7 +722,6 @@ export function annotateKey(annotations: Annotations.Key {} - /** * Lightweight structural constraint for APIs that accept schema values but only * read their data and type-level views. @@ -803,7 +801,6 @@ export interface Constraint { readonly "~encoded.optionality": Optionality readonly "~encoded.mutability": Mutability } - /** * Lightweight structural constraint for APIs that need codec type views but do * not need the full schema protocol. @@ -826,7 +823,6 @@ export interface ConstraintCodec extends ConstraintCodec {} - /** * Lightweight structural constraint for APIs that need encoder type views but * do not need the full schema protocol. @@ -864,7 +859,6 @@ export interface ConstraintDecoder extends ConstraintCode * @since 4.0.0 */ export interface ConstraintEncoder extends ConstraintCodec {} - /** * Lightweight structural constraint for APIs that need schema views and the * rebuilt schema type, but do not call the full schema protocol. @@ -881,7 +875,6 @@ export interface ConstraintEncoder extends ConstraintCode export interface ConstraintRebuildable extends Constraint { readonly "Rebuild": Constraint } - /** * Namespace of type-level helpers for {@link Schema}. * @@ -904,9 +897,11 @@ export declare namespace Schema { * @category utility types * @since 3.10.0 */ - export type Type = S extends { readonly "Type": infer T } ? T : never + type Type = S extends { + readonly "Type": infer T + } ? T : + never } - /** * A typed view of a schema that tracks only the decoded (output) type `T`. * @@ -941,7 +936,6 @@ export interface Schema extends Top { readonly "Type": T readonly "Rebuild": Schema } - /** * Namespace of type-level helpers for {@link Codec}. * @@ -964,8 +958,10 @@ export declare namespace Codec { * @category utility types * @since 3.10.0 */ - export type Encoded = S extends { readonly "Encoded": infer E } ? E : never - + type Encoded = S extends { + readonly "Encoded": infer E + } ? E : + never /** * Extracts the Effect services required during *decoding* from a schema. * @@ -982,8 +978,10 @@ export declare namespace Codec { * @category utility types * @since 4.0.0 */ - export type DecodingServices = S extends { readonly "DecodingServices": infer R } ? R : never - + type DecodingServices = S extends { + readonly "DecodingServices": infer R + } ? R : + never /** * Extracts the Effect services required during *encoding* from a schema. * @@ -1000,9 +998,11 @@ export declare namespace Codec { * @category utility types * @since 4.0.0 */ - export type EncodingServices = S extends { readonly "EncodingServices": infer R } ? R : never + type EncodingServices = S extends { + readonly "EncodingServices": infer R + } ? R : + never } - /** * A schema that tracks the decoded type `T`, the encoded type `E`, and the * Effect services required during decoding (`RD`) and encoding (`RE`). @@ -1043,7 +1043,6 @@ export interface Codec extends readonly "EncodingServices": RE readonly "Rebuild": Codec } - /** * A schema that tracks the decoded type `T` and the Effect services required * during decoding (`RD`). @@ -1066,7 +1065,6 @@ export interface Decoder extends Schema { readonly "EncodingServices": unknown readonly "Rebuild": Decoder } - /** * A schema that tracks the encoded type `E` and the Effect services required * during encoding (`RE`). @@ -1089,7 +1087,6 @@ export interface Encoder extends Schema { readonly "EncodingServices": RE readonly "Rebuild": Encoder } - /** * Returns a codec widened to the full {@link Codec} interface, prompting * TypeScript to infer all four type parameters (`T`, `E`, `RD`, `RE`). @@ -1118,7 +1115,6 @@ export interface Encoder extends Schema { export function revealCodec(codec: Codec) { return codec } - /** * A schema that additionally supports optic (lens/prism) operations. * @@ -1143,9 +1139,6 @@ export interface Optic extends Schema { readonly "EncodingServices": never readonly "Rebuild": Optic } - -const SchemaErrorTypeId = "~effect/Schema/SchemaError" - /** * Error thrown or returned when schema decoding or encoding fails. * @@ -1196,7 +1189,6 @@ export class SchemaError extends Data.TaggedError("SchemaError")<{ return `SchemaError(${this.message})` } } - /** * Returns `true` if `u` is a {@link SchemaError}. * @@ -1218,15 +1210,64 @@ export class SchemaError extends Data.TaggedError("SchemaError")<{ * @since 4.0.0 */ export function isSchemaError(u: unknown): u is SchemaError { - return Predicate.hasProperty(u, SchemaErrorTypeId) && u[SchemaErrorTypeId] === SchemaErrorTypeId + return isSchemaErrorInternal(u) +} + +function fromIssueEffect( + self: Effect.Effect +): Effect.Effect { + if (effectIsExit(self)) { + return fromIssueExit(self as Exit_.Exit) + } + return Effect.catchCause( + self, + (cause) => Effect.failCauseSync(() => Cause_.map(cause, (issue) => new SchemaError(issue))) + ) +} + +function fromIssueExit(exit: Exit_.Exit): Exit_.Exit { + return Exit_.isSuccess(exit) + ? exit as unknown as Exit_.Exit + : Exit_.failCause(Cause_.map(exit.cause, (issue) => new SchemaError(issue))) } -function makeStandardResult(exit: Exit_.Exit>): StandardSchemaV1.Result { - return Exit_.isSuccess(exit) ? exit.value : { - issues: [{ message: Cause_.pretty(exit.cause) }] +function getSchemaErrorOrThrow( + cause: Cause_.Cause, + message: string +): SchemaError { + let schemaError: SchemaError | undefined + for (const reason of cause.reasons) { + if (!Cause_.isFailReason(reason) || !isSchemaError(reason.error)) { + throw new globalThis.Error(message, { cause }) + } + schemaError ??= reason.error + } + if (schemaError === undefined) { + throw new globalThis.Error(message, { cause }) } + return schemaError +} + +function runSchemaErrorPromise( + self: Effect.Effect +): Promise { + return Effect.runPromiseExit(self).then((exit) => { + if (Exit_.isSuccess(exit)) { + return exit.value + } + throw getSchemaErrorOrThrow(exit.cause, "Promise adapter can only reject schema errors") + }) } +function runSchemaErrorSync( + self: Effect.Effect +): A { + const exit = Effect.runSyncExit(self) + if (Exit_.isSuccess(exit)) { + return exit.value + } + throw getSchemaErrorOrThrow(exit.cause, "Sync adapter can only throw schema errors") +} /** * Returns a "Standard Schema" object conforming to the [Standard Schema * v1](https://standardschema.dev/) specification. @@ -1303,67 +1344,8 @@ export function toStandardSchemaV1>( readonly parseOptions?: SchemaAST.ParseOptions | undefined } ): StandardSchemaV1 & S { - const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(self) as ( - input: unknown, - options?: SchemaAST.ParseOptions - ) => Effect.Effect - 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("sync") - const fiber = Effect.runFork( - Effect.match(decodeUnknownEffect(value, parseOptions), { - onFailure: formatter, - onSuccess: (value): StandardSchemaV1.Result => ({ value }) - }), - { scheduler } - ) - fiber.currentDispatcher?.flush() - const exit = fiber.pollUnsafe() - if (exit) { - return makeStandardResult(exit) - } - return new Promise((resolve) => { - fiber.addObserver((exit) => { - resolve(makeStandardResult(exit)) - }) - }) - } - if ("~standard" in self) { - const out = self as any - if ("validate" in out["~standard"]) return out - Object.assign(out["~standard"], { validate }) - return out - } else { - return Object.assign(self, { - "~standard": { - version: 1, - vendor: "effect", - validate - } as const - }) - } -} - -function toBaseStandardJSONSchemaV1(self: Constraint, target: StandardJSONSchemaV1.Target): JsonSchema.JsonSchema { - const doc2020_12 = toJsonSchemaDocument(self) - if (target === "draft-2020-12") { - const schema = doc2020_12.schema - if (Object.keys(doc2020_12.definitions).length > 0) { - schema.$defs = doc2020_12.definitions - } - return schema - } else if (target === "draft-07") { - const doc07 = JsonSchema.toDocumentDraft07(doc2020_12) - const schema = doc07.schema - if (Object.keys(doc07.definitions).length > 0) { - schema.definitions = doc07.definitions - } - return schema - } - throw new globalThis.Error(`Unsupported target: ${target}`) + return InternalStandardSchema.toStandardSchemaV1(self, options) } - /** * Converts a schema to an experimental Standard JSON Schema V1 representation. * @@ -1377,30 +1359,8 @@ function toBaseStandardJSONSchemaV1(self: Constraint, target: StandardJSONSchema export function toStandardJSONSchemaV1( self: S ): StandardJSONSchemaV1 & S { - const jsonSchema: StandardJSONSchemaV1.Props["jsonSchema"] = { - input(options) { - return toBaseStandardJSONSchemaV1(self, options.target) - }, - output(options) { - return toBaseStandardJSONSchemaV1(toType(self), options.target) - } - } - if ("~standard" in self) { - const out = self as any - if ("jsonSchema" in out["~standard"]) return out - Object.assign(out["~standard"], { jsonSchema }) - return out - } else { - return Object.assign(self, { - "~standard": { - version: 1, - vendor: "effect", - jsonSchema - } as const - }) - } + return InternalStandardSchema.toStandardJSONSchemaV1(self) } - /** * Creates a type guard function that checks if a value conforms to a given * schema. @@ -1438,8 +1398,7 @@ export function toStandardJSONSchemaV1( * @category guards * @since 3.10.0 */ -export const is = SchemaParser.is - +export const is: typeof SchemaParser.is = SchemaParser.is /** * Creates an assertion function that throws an error if the input does not match * the schema. @@ -1490,7 +1449,6 @@ export const is = SchemaParser.is */ export const asserts: (schema: S, input: I) => asserts input is I & S["Type"] = SchemaParser.asserts - /** * Decodes an `unknown` input against a schema, returning an `Effect` that * succeeds with the decoded value or fails with a {@link SchemaError}. @@ -1521,19 +1479,6 @@ export function decodeUnknownEffect(schema: S, options?: S return fromIssueEffect(parser(input, options)) } } - -function fromIssueEffect( - self: Effect.Effect -): Effect.Effect { - if (effectIsExit(self)) { - return fromIssueExit(self as Exit_.Exit) - } - 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 @@ -1562,45 +1507,6 @@ export const decodeEffect: ( input: S["Encoded"], options?: SchemaAST.ParseOptions ) => Effect.Effect = decodeUnknownEffect - -function getSchemaErrorOrThrow( - cause: Cause_.Cause, - message: string -): SchemaError { - let schemaError: SchemaError | undefined - for (const reason of cause.reasons) { - if (!Cause_.isFailReason(reason) || !isSchemaError(reason.error)) { - throw new globalThis.Error(message, { cause }) - } - schemaError ??= reason.error - } - if (schemaError === undefined) { - throw new globalThis.Error(message, { cause }) - } - return schemaError -} - -function runSchemaErrorPromise( - self: Effect.Effect -): Promise { - return Effect.runPromiseExit(self).then((exit) => { - if (Exit_.isSuccess(exit)) { - return exit.value - } - throw getSchemaErrorOrThrow(exit.cause, "Promise adapter can only reject schema errors") - }) -} - -function runSchemaErrorSync( - self: Effect.Effect -): A { - const exit = Effect.runSyncExit(self) - if (Exit_.isSuccess(exit)) { - return exit.value - } - throw getSchemaErrorOrThrow(exit.cause, "Sync adapter can only throw schema errors") -} - /** * Decodes an `unknown` input against a schema synchronously, returning an * `Exit` that is either a `Success` with the decoded value or a `Failure`. @@ -1637,13 +1543,6 @@ export function decodeUnknownExit>(schema: return fromIssueExit(parser(input, options)) } } - -function fromIssueExit(exit: Exit_.Exit): Exit_.Exit { - return Exit_.isSuccess(exit) - ? exit as unknown as Exit_.Exit - : Exit_.failCause(Cause_.map(exit.cause, (issue) => new SchemaError(issue))) -} - /** * Decodes a typed input (the schema's `Encoded` type) against a schema * synchronously, returning an `Exit` that is either a `Success` with the decoded @@ -1678,7 +1577,6 @@ export const decodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Exit_.Exit = decodeUnknownExit - /** * Decodes an `unknown` input against a schema, returning an `Option` that is * `Some` with the decoded value on success or `None` for schema mismatches. @@ -1709,7 +1607,6 @@ export const decodeUnknownOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: unknown, options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.decodeUnknownOption - /** * Decodes a typed input (the schema's `Encoded` type) against a schema, * returning an `Option` that is `Some` with the decoded value on success or @@ -1739,7 +1636,6 @@ export const decodeOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.decodeOption - /** * Decodes an `unknown` input against a schema, returning a `Result` that * succeeds with the decoded value or fails with a {@link SchemaError} for schema @@ -1775,7 +1671,6 @@ export function decodeUnknownResult>(schema return Result_.mapError(parser(input, options), (issue) => new SchemaError(issue)) } } - /** * Decodes a typed input (the schema's `Encoded` type) against a schema, * returning a `Result` that succeeds with the decoded value or fails with a @@ -1809,7 +1704,6 @@ export const decodeResult: >( options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Result_.Result = decodeUnknownResult - /** * Decodes an `unknown` input against a schema, returning a `Promise` that * resolves with the decoded value or rejects with a {@link SchemaError} for @@ -1846,7 +1740,6 @@ export function decodeUnknownPromise>( return runSchemaErrorPromise(parser(input, options)) } } - /** * Decodes a typed input (the schema's `Encoded` type) against a schema, * returning a `Promise` that resolves with the decoded value or rejects with a @@ -1878,7 +1771,6 @@ export const decodePromise: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Promise = decodeUnknownPromise - /** * Decodes an `unknown` input against a schema synchronously, returning the * decoded value or throwing a {@link SchemaError} for schema mismatches. @@ -1922,7 +1814,6 @@ export function decodeUnknownSync>(schema: return runSchemaErrorSync(parser(input, options)) } } - /** * Decodes a typed input (the schema's `Encoded` type) against a schema * synchronously, returning the decoded value or throwing a {@link SchemaError} @@ -1953,7 +1844,6 @@ export const decodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => S["Type"] = decodeUnknownSync - /** * Encodes an `unknown` input against a schema, returning an `Effect` that * succeeds with the encoded value or fails with a {@link SchemaError}. @@ -1994,7 +1884,6 @@ export function encodeUnknownEffect(schema: S, options?: S return fromIssueEffect(parser(input, options)) } } - /** * Encodes a typed input (the schema's `Type`) against a schema, returning an * `Effect` that succeeds with the encoded value or fails with a @@ -2023,7 +1912,6 @@ export const encodeEffect: ( input: S["Type"], options?: SchemaAST.ParseOptions ) => Effect.Effect = encodeUnknownEffect - /** * Encodes an `unknown` input against a schema synchronously, returning an * `Exit` that is either a `Success` with the encoded value or a `Failure`. @@ -2059,7 +1947,6 @@ export function encodeUnknownExit>(schema: return fromIssueExit(parser(input, options)) } } - /** * Encodes a typed input (the schema's `Type`) against a schema synchronously, * returning an `Exit` that is either a `Success` with the encoded value or a @@ -2094,7 +1981,6 @@ export const encodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Exit_.Exit = encodeUnknownExit - /** * Encodes an `unknown` input against a schema, returning an `Option` that is * `Some` with the encoded value on success or `None` for schema mismatches. @@ -2126,7 +2012,6 @@ export const encodeUnknownOption: >( options?: SchemaAST.ParseOptions ) => (input: unknown, options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.encodeUnknownOption - /** * Encodes a typed input (the schema's `Type`) against a schema, returning an * `Option` that is `Some` with the encoded value on success or `None` for schema @@ -2156,7 +2041,6 @@ export const encodeOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.encodeOption - /** * Encodes an `unknown` input against a schema, returning a `Result` that * succeeds with the encoded value or fails with a {@link SchemaError} for schema @@ -2191,7 +2075,6 @@ export function encodeUnknownResult>(schema return Result_.mapError(parser(input, options), (issue) => new SchemaError(issue)) } } - /** * Encodes a typed input (the schema's `Type`) against a schema, returning a * `Result` that succeeds with the encoded value or fails with a @@ -2225,7 +2108,6 @@ export const encodeResult: >( options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Result_.Result = encodeUnknownResult - /** * Encodes an `unknown` input against a schema, returning a `Promise` that * resolves with the encoded value or rejects with a {@link SchemaError} for @@ -2261,7 +2143,6 @@ export function encodeUnknownPromise>( return runSchemaErrorPromise(parser(input, options)) } } - /** * Encodes a typed input (the schema's `Type`) against a schema, returning a * `Promise` that resolves with the encoded value or rejects with a @@ -2293,7 +2174,6 @@ export const encodePromise: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Promise = encodeUnknownPromise - /** * Encodes an `unknown` input against a schema synchronously, throwing a * {@link SchemaError} for schema mismatches. @@ -2326,7 +2206,6 @@ export function encodeUnknownSync>(schema: return runSchemaErrorSync(parser(input, options) as Effect.Effect) } } - /** * Encodes a typed input (the schema's `Type`) against a schema synchronously, * throwing a {@link SchemaError} for schema mismatches. @@ -2355,7 +2234,6 @@ export const encodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => S["Encoded"] = encodeUnknownSync - /** * Creates a schema from an AST (Abstract Syntax Tree) node. * @@ -2373,8 +2251,7 @@ export const encodeSync: >( * @category constructors * @since 3.10.0 */ -export const make: (ast: S["ast"], options?: object) => S = InternalSchema.make - +export const make: (ast: S["ast"], options?: object) => S = InternalMake.make /** * Checks whether a value is a `Schema`. * @@ -2384,7 +2261,6 @@ export const make: (ast: S["ast"], options?: object) => S export function isSchema(u: unknown): u is Top { return Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId } - /** * Type-level representation returned by {@link optionalKey}. * @@ -2412,12 +2288,10 @@ export interface optionalKey extends readonly "Iso": S["Iso"] readonly schema: S } - interface optionalKeyLambda extends Lambda { (self: S): optionalKey readonly "~lambda.out": this["~lambda.in"] extends Constraint ? optionalKey : never } - /** * Creates an exact optional key schema for struct fields. Unlike `optional`, * this creates exact optional properties (not `| undefined`) that can be @@ -2440,16 +2314,14 @@ interface optionalKeyLambda extends Lambda { * @category combinators * @since 4.0.0 */ -export const optionalKey = Struct_.lambda((schema) => +export const optionalKey: optionalKeyLambda = Struct_.lambda((schema) => make(SchemaAST.optionalKey(schema.ast), { schema }) ) - interface requiredKeyLambda extends Lambda { (self: optionalKey): S readonly "~lambda.out": this["~lambda.in"] extends optionalKey ? this["~lambda.in"]["schema"] : "Error: schema not eligible for requiredKey" } - /** * Reverses `optionalKey` and returns the inner required schema. * @@ -2461,8 +2333,7 @@ interface requiredKeyLambda extends Lambda { * @category combinators * @since 4.0.0 */ -export const requiredKey = Struct_.lambda((self) => self.schema) - +export const requiredKey: requiredKeyLambda = Struct_.lambda((self) => self.schema) /** * Type-level representation returned by {@link optional}. * @@ -2472,12 +2343,10 @@ export const requiredKey = Struct_.lambda((self) => self.sche export interface optional extends optionalKey> { readonly "Rebuild": optional } - interface optionalLambda extends Lambda { (self: S): optional readonly "~lambda.out": this["~lambda.in"] extends Constraint ? optional : never } - /** * Marks a struct field as optional, allowing the key to be absent or * `undefined`. @@ -2507,17 +2376,15 @@ interface optionalLambda extends Lambda { * @category combinators * @since 3.10.0 */ -export const optional = Struct_.lambda((self) => { +export const optional: optionalLambda = Struct_.lambda((self) => { const schema = UndefinedOr(self) return make(SchemaAST.optional(self.ast), { schema }) }) - interface requiredLambda extends Lambda { (self: optional): S readonly "~lambda.out": this["~lambda.in"] extends optional ? this["~lambda.in"]["schema"]["members"][0] : "Error: schema not eligible for required" } - /** * Reverses `optional` and returns the inner schema. * @@ -2533,8 +2400,7 @@ interface requiredLambda extends Lambda { * @category combinators * @since 3.10.0 */ -export const required = Struct_.lambda((self) => self.schema.members[0]) - +export const required: requiredLambda = Struct_.lambda((self) => self.schema.members[0]) /** * Type-level representation returned by {@link mutableKey}. * @@ -2562,12 +2428,10 @@ export interface mutableKey extends readonly "Iso": S["Iso"] readonly schema: S } - interface mutableKeyLambda extends Lambda { (self: S): mutableKey readonly "~lambda.out": this["~lambda.in"] extends Constraint ? mutableKey : never } - /** * Makes a struct field mutable (removes the `readonly` modifier on the property). * Use {@link readonlyKey} to reverse. @@ -2575,16 +2439,14 @@ interface mutableKeyLambda extends Lambda { * @category combinators * @since 4.0.0 */ -export const mutableKey = Struct_.lambda((schema) => +export const mutableKey: mutableKeyLambda = Struct_.lambda((schema) => make(SchemaAST.mutableKey(schema.ast), { schema }) ) - interface readonlyKeyLambda extends Lambda { (self: mutableKey): S readonly "~lambda.out": this["~lambda.in"] extends mutableKey ? this["~lambda.in"]["schema"] : "Error: schema not eligible for readonlyKey" } - /** * Reverses `mutableKey` and returns the inner readonly schema. * @@ -2596,8 +2458,7 @@ interface readonlyKeyLambda extends Lambda { * @category combinators * @since 4.0.0 */ -export const readonlyKey = Struct_.lambda((self) => self.schema) - +export const readonlyKey: readonlyKeyLambda = Struct_.lambda((self) => self.schema) /** * Type-level representation returned by {@link toType}. * @@ -2625,12 +2486,10 @@ export interface toType extends readonly "Iso": S["Iso"] readonly schema: S } - interface toTypeLambda extends Lambda { (self: S): toType readonly "~lambda.out": this["~lambda.in"] extends Constraint ? toType : never } - /** * Extracts the type-side schema: sets `Encoded` to equal the decoded `Type`, * discarding the encoding transformation path. @@ -2638,8 +2497,9 @@ interface toTypeLambda extends Lambda { * @category transforming * @since 4.0.0 */ -export const toType = Struct_.lambda((schema) => make(SchemaAST.toType(schema.ast), { schema })) - +export const toType: toTypeLambda = Struct_.lambda((schema) => + make(SchemaAST.toType(schema.ast), { schema }) +) /** * Type-level representation returned by {@link toEncoded}. * @@ -2667,12 +2527,10 @@ export interface toEncoded extends readonly "Iso": S["Encoded"] readonly schema: S } - interface toEncodedLambda extends Lambda { (self: S): toEncoded readonly "~lambda.out": this["~lambda.in"] extends Constraint ? toEncoded : never } - /** * Extracts the encoded-side schema: sets `Type` to equal the `Encoded`, * discarding the decoding transformation path. @@ -2680,10 +2538,10 @@ interface toEncodedLambda extends Lambda { * @category transforming * @since 4.0.0 */ -export const toEncoded = Struct_.lambda((schema) => make(SchemaAST.toEncoded(schema.ast), { schema })) - +export const toEncoded: toEncodedLambda = Struct_.lambda((schema) => + make(SchemaAST.toEncoded(schema.ast), { schema }) +) const FlipTypeId = "~effect/Schema/flip" - /** * Type-level representation returned by {@link flip}. * @@ -2716,7 +2574,6 @@ export interface flip extends function isFlip$(schema: Top): schema is flip { return Predicate.hasProperty(schema, FlipTypeId) && schema[FlipTypeId] === FlipTypeId } - /** * Swaps the decoded and encoded sides of a schema. * @@ -2748,7 +2605,6 @@ export function flip(schema: S): flip { } return make(SchemaAST.flip(schema.ast), { [FlipTypeId]: FlipTypeId, schema }) } - /** * Type-level representation returned by {@link Literal}. * @@ -2761,7 +2617,6 @@ export interface Literal readonly literal: L transform(to: L2): decodeTo, Literal> } - /** * Creates a schema for a single literal value (string, number, bigint, boolean, or null). * @@ -2793,7 +2648,6 @@ export function Literal(literal: L): Literal - - type AppendType< - Template extends string, - Next - > = Next extends LiteralPart ? `${Template}${Next}` - : Next extends { readonly Encoded: infer E extends LiteralPart } ? `${Template}${E}` - : never - + type Parts = ReadonlyArray + type AppendType