diff --git a/.changeset/readd-stream-sql-pg.md b/.changeset/readd-stream-sql-pg.md deleted file mode 100644 index 9f7a982fbc3..00000000000 --- a/.changeset/readd-stream-sql-pg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@effect/sql-pg": patch ---- - -Readded stream as an optional parameter to PgClientConfig. diff --git a/.github/workflows/ts-nightly.yml b/.github/workflows/ts-nightly.yml index d5da61457f9..ed8d2bdcddc 100644 --- a/.github/workflows/ts-nightly.yml +++ b/.github/workflows/ts-nightly.yml @@ -9,6 +9,8 @@ permissions: {} jobs: types: name: Types + # TODO enable after TSTyche adds TypeScript 7 support (see https://github.com/tstyche/tstyche/issues/443) + if: false runs-on: ubuntu-latest timeout-minutes: 10 permissions: diff --git a/.gitignore b/.gitignore index 8feab8b7cc1..8682277c305 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ scratchpad/ .direnv/ .idea/ .env* +.lalph/ +.repos/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..72fea10f29f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,77 @@ +# Agent Instructions + +This is the Effect library repository, focusing on functional programming patterns and effect systems in TypeScript. + +## Development Workflow + +- The git base branch is `main` +- Use `pnpm` as the package manager + +### Core Principles + +- **Zero Tolerance for Errors**: All automated checks must pass +- **Clarity over Cleverness**: Choose clear, maintainable solutions +- **Conciseness**: Keep code and any wording concise and to the point. Sacrifice grammar for the sake of concision. +- **Reduce comments**: Avoid comments unless absolutely required to explain unusual or complex logic. Comments in jsdocs are acceptable. + +### Mandatory Validation Steps + +- Run `pnpm lint-fix` after editing files +- Always run tests after making changes: `pnpm test run ` +- Run type checking: `pnpm check` + - If type checking continues to fail, run `pnpm clean` to clear caches, then re-run `pnpm check` +- Build the project: `pnpm build` +- Check JSDoc examples compile: `pnpm docgen` + +## Code Style Guidelines + +**Always** look at existing code in the repository to learn and follow +established patterns before writing new code. + +Do not worry about getting code formatting perfect while writing. Use `pnpm lint-fix` +to automatically format code according to the project's style guidelines. + +### Barrel files + +The `index.ts` files are automatically generated. Do not manually edit them. Use +`pnpm codegen` to regenerate barrel files after adding or removing modules. + +## Changesets + +All pull requests must include a changeset in the `.changeset/` directory. +This is important for maintaining a clear changelog and ensuring proper versioning of packages. + +## Running test code + +If you need to run some code for testing or debugging purposes, create a new +file in the `scratchpad/` directory at the root of the repository. You can then +run the file with `tsx scratchpad/your-file.ts`. + +Make sure to delete the file after you are done testing. + +## Testing + +Before writing tests, look at existing tests in the codebase for similar +functionality to follow established patterns. + +- Test files are located in `packages/*/test/` directories for each package +- Main Effect library tests: `packages/effect/test/` +- Always verify implementations with tests +- Run specific tests with: `pnpm test ` + +### it.effect Testing Pattern + +- Use `it.effect` for all Effect-based tests, not `Effect.runSync` with regular `it` +- Import `{ assert, describe, it }` from `@effect/vitest` +- Never use `expect` from vitest in Effect tests - use `assert` methods instead +- All tests should use `it.effect("description", () => Effect.gen(function*() { ... }))` + +Before writing tests, look at existing tests in the codebase for similar +functionality to follow established patterns. + +## Learning about "effect" v4 + +If you need to learn more about the new version of effect (version 4), you can +access the repository here: + +\`.repos/effect-v4\` diff --git a/eslint.config.mjs b/eslint.config.mjs index c05998a035c..ff0e6bdf969 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,7 +20,14 @@ const compat = new FlatCompat({ export default [ { - ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] + ignores: [ + "**/dist", + "**/build", + "**/docs", + "**/.repos/**", + "**/.lalph/**", + "**/*.md" + ] }, ...compat.extends( "eslint:recommended", @@ -65,7 +72,8 @@ export default [ "no-restricted-syntax": [ "error", { - selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", + selector: + "CallExpression[callee.property.name='push'] > SpreadElement.arguments", message: "Do not use spread arguments in Array.push" } ], @@ -145,11 +153,7 @@ export default [ "@effect/no-import-from-barrel-package": [ "error", { - packageNames: [ - "effect", - "@effect/platform", - "@effect/sql" - ] + packageNames: ["effect", "@effect/platform", "@effect/sql"] } ] } diff --git a/package.json b/package.json index f253793854f..d1581ec06fd 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@changesets/cli": "^2.29.4", "@edge-runtime/vm": "^5.0.0", "@effect/build-utils": "^0.8.3", - "@effect/docgen": "https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@fd06738", + "@effect/docgen": "https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e7fe055", "@effect/eslint-plugin": "^0.3.2", "@effect/language-service": "^0.23.3", "@effect/vitest": "workspace:^", @@ -91,7 +91,10 @@ "workerd" ], "onlyBuiltDependencies": [ - "better-sqlite3" + "@parcel/watcher", + "better-sqlite3", + "sharp", + "unrs-resolver" ] } } diff --git a/packages/ai/ai/CHANGELOG.md b/packages/ai/ai/CHANGELOG.md index 85302cde249..eee8a0fb7d6 100644 --- a/packages/ai/ai/CHANGELOG.md +++ b/packages/ai/ai/CHANGELOG.md @@ -1,5 +1,29 @@ # @effect/ai +## 0.35.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/rpc@0.75.0 + +## 0.34.0 + +### Patch Changes + +- [#6094](https://github.com/Effect-TS/effect/pull/6094) [`88a5260`](https://github.com/Effect-TS/effect/commit/88a5260788d57bb0c31a645fa44b5a68f5c73c38) Thanks @IMax153! - Remove superfluous / defensive check from tool call json schema generation + +- [#6101](https://github.com/Effect-TS/effect/pull/6101) [`cf74940`](https://github.com/Effect-TS/effect/commit/cf749405214282edc916a783b3417c766d2bbfe3) Thanks @IMax153! - Prevent schema validation when directly constructing an `AiError.HttpRequestError` / `AiError.HttpResponseError` + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/rpc@0.74.0 + ## 0.33.2 ### Patch Changes diff --git a/packages/ai/ai/package.json b/packages/ai/ai/package.json index 76d375914ff..9d737627475 100644 --- a/packages/ai/ai/package.json +++ b/packages/ai/ai/package.json @@ -1,7 +1,7 @@ { "name": "@effect/ai", "type": "module", - "version": "0.33.2", + "version": "0.35.0", "license": "MIT", "description": "Effect modules for working with AI apis", "homepage": "https://effect.website", diff --git a/packages/ai/ai/src/AiError.ts b/packages/ai/ai/src/AiError.ts index 80ad483b65d..f87687ea291 100644 --- a/packages/ai/ai/src/AiError.ts +++ b/packages/ai/ai/src/AiError.ts @@ -162,7 +162,8 @@ export const HttpRequestDetails = Schema.Struct({ * @example * ```ts * import { AiError } from "@effect/ai" - * import { Effect } from "effect" + * import * as Effect from "effect/Effect" + * import * as Option from "effect/Option" * * const handleNetworkError = Effect.gen(function* () { * const error = new AiError.HttpRequestError({ @@ -240,7 +241,7 @@ export class HttpRequestError extends Schema.TaggedError( url: error.request.url, urlParams: error.request.urlParams } - }) + }, { disableValidation: true }) } get message(): string { @@ -412,7 +413,7 @@ export class HttpResponseError extends Schema.TaggedError( status: error.response.status }, body: Inspectable.format(body) - })) + }, { disableValidation: true })) } get message(): string { @@ -466,9 +467,9 @@ export class HttpResponseError extends Schema.TaggedError( * @example * ```ts * import { AiError } from "@effect/ai" - * import { Effect } from "effect" + * import * as Effect from "effect/Effect" * - * const validateInput = (data: unknown) => + * const validateInput = (data: unknown): Effect.Effect => * typeof data === "string" && data.length > 0 * ? Effect.succeed(data) * : Effect.fail(new AiError.MalformedInput({ diff --git a/packages/ai/ai/src/Chat.ts b/packages/ai/ai/src/Chat.ts index b6a3b250c9d..656944a623c 100644 --- a/packages/ai/ai/src/Chat.ts +++ b/packages/ai/ai/src/Chat.ts @@ -78,10 +78,10 @@ import type * as Tool from "./Tool.js" * @example * ```ts * import { Chat } from "@effect/ai" - * import { Effect } from "effect" + * import * as Effect from "effect/Effect" * * const useChat = Effect.gen(function* () { - * const chat = yield* Chat + * const chat = yield* Chat.Chat * const response = yield* chat.generateText({ * prompt: "Explain quantum computing in simple terms" * }) diff --git a/packages/ai/ai/src/EmbeddingModel.ts b/packages/ai/ai/src/EmbeddingModel.ts index acf4f67d4ee..8054a7b0584 100644 --- a/packages/ai/ai/src/EmbeddingModel.ts +++ b/packages/ai/ai/src/EmbeddingModel.ts @@ -69,10 +69,17 @@ import * as AiError from "./AiError.js" * @example * ```ts * import { EmbeddingModel } from "@effect/ai" - * import { Effect } from "effect" + * import * as Effect from "effect/Effect" + * + * const cosineSimilarity = (a: ReadonlyArray, b: ReadonlyArray): number => { + * const dot = a.reduce((sum, ai, i) => sum + ai * (b[i] ?? 0), 0) + * const normA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0)) + * const normB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0)) + * return normA === 0 || normB === 0 ? 0 : dot / (normA * normB) + * } * * const useEmbeddings = Effect.gen(function* () { - * const embedder = yield* EmbeddingModel + * const embedder = yield* EmbeddingModel.EmbeddingModel * * const documentVector = yield* embedder.embed("This is a sample document") * const queryVector = yield* embedder.embed("sample query") diff --git a/packages/ai/ai/src/LanguageModel.ts b/packages/ai/ai/src/LanguageModel.ts index c217eb2d204..c95c460bde1 100644 --- a/packages/ai/ai/src/LanguageModel.ts +++ b/packages/ai/ai/src/LanguageModel.ts @@ -83,10 +83,10 @@ import * as Toolkit from "./Toolkit.js" * @example * ```ts * import { LanguageModel } from "@effect/ai" - * import { Effect } from "effect" + * import * as Effect from "effect/Effect" * * const useLanguageModel = Effect.gen(function* () { - * const model = yield* LanguageModel + * const model = yield* LanguageModel.LanguageModel * const response = yield* model.generateText({ * prompt: "What is machine learning?" * }) diff --git a/packages/ai/ai/src/Telemetry.ts b/packages/ai/ai/src/Telemetry.ts index 007928a2b68..d92f94042fb 100644 --- a/packages/ai/ai/src/Telemetry.ts +++ b/packages/ai/ai/src/Telemetry.ts @@ -519,11 +519,13 @@ export interface SpanTransformer { * @example * ```ts * import { Telemetry } from "@effect/ai" - * import { Context, Effect } from "effect" + * import * as Effect from "effect/Effect" + * + * declare const myAIOperation: Effect.Effect * * // Create a custom span transformer * const loggingTransformer: Telemetry.SpanTransformer = (options) => { - * console.log(`AI request completed: ${options.model}`) + * console.log(`AI request completed: ${options.response.length} part(s)`) * options.response.forEach((part, index) => { * console.log(`Part ${index}: ${part.type}`) * }) diff --git a/packages/ai/ai/src/Tool.ts b/packages/ai/ai/src/Tool.ts index 3dcb012d5cc..05128068f5d 100644 --- a/packages/ai/ai/src/Tool.ts +++ b/packages/ai/ai/src/Tool.ts @@ -1297,15 +1297,6 @@ export const getJsonSchema = < export const getJsonSchemaFromSchemaAst = ( ast: AST.AST ): JsonSchema.JsonSchema7 => { - const props = AST.getPropertySignatures(ast) - if (props.length === 0) { - return { - type: "object", - properties: {}, - required: [], - additionalProperties: false - } - } const $defs = {} const schema = JsonSchema.fromAST(ast, { definitions: $defs, diff --git a/packages/ai/amazon-bedrock/CHANGELOG.md b/packages/ai/amazon-bedrock/CHANGELOG.md index 27a29d7601a..06ed0523e29 100644 --- a/packages/ai/amazon-bedrock/CHANGELOG.md +++ b/packages/ai/amazon-bedrock/CHANGELOG.md @@ -1,5 +1,27 @@ # @effect/ai-amazon-bedrock +## 0.15.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/ai-anthropic@0.25.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.14.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/ai-anthropic@0.24.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + ## 0.13.0 ### Patch Changes diff --git a/packages/ai/amazon-bedrock/package.json b/packages/ai/amazon-bedrock/package.json index 63a3f4d8c87..efbae14f683 100644 --- a/packages/ai/amazon-bedrock/package.json +++ b/packages/ai/amazon-bedrock/package.json @@ -1,7 +1,7 @@ { "name": "@effect/ai-amazon-bedrock", "type": "module", - "version": "0.13.0", + "version": "0.15.0", "license": "MIT", "description": "Effect modules for working with Amazon Bedrock AI apis", "homepage": "https://effect.website", diff --git a/packages/ai/anthropic/CHANGELOG.md b/packages/ai/anthropic/CHANGELOG.md index 597fee61837..4e3a897ad14 100644 --- a/packages/ai/anthropic/CHANGELOG.md +++ b/packages/ai/anthropic/CHANGELOG.md @@ -1,5 +1,25 @@ # @effect/ai-anthropic +## 0.25.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.24.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + ## 0.23.0 ### Patch Changes diff --git a/packages/ai/anthropic/package.json b/packages/ai/anthropic/package.json index ef00848e651..3ec630786b4 100644 --- a/packages/ai/anthropic/package.json +++ b/packages/ai/anthropic/package.json @@ -1,7 +1,7 @@ { "name": "@effect/ai-anthropic", "type": "module", - "version": "0.23.0", + "version": "0.25.0", "license": "MIT", "description": "Effect modules for working with AI apis", "homepage": "https://effect.website", diff --git a/packages/ai/google/CHANGELOG.md b/packages/ai/google/CHANGELOG.md index 8c632dca00a..b7fcb78c582 100644 --- a/packages/ai/google/CHANGELOG.md +++ b/packages/ai/google/CHANGELOG.md @@ -1,5 +1,25 @@ # @effect/ai-google +## 0.14.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.13.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + ## 0.12.1 ### Patch Changes diff --git a/packages/ai/google/package.json b/packages/ai/google/package.json index 60381f8e90a..5d2d2e791da 100644 --- a/packages/ai/google/package.json +++ b/packages/ai/google/package.json @@ -1,7 +1,7 @@ { "name": "@effect/ai-google", "type": "module", - "version": "0.12.1", + "version": "0.14.0", "license": "MIT", "description": "Effect modules for working with AI apis", "homepage": "https://effect.website", diff --git a/packages/ai/openai/CHANGELOG.md b/packages/ai/openai/CHANGELOG.md index 59514162e29..a39780a9d58 100644 --- a/packages/ai/openai/CHANGELOG.md +++ b/packages/ai/openai/CHANGELOG.md @@ -1,5 +1,44 @@ # @effect/ai-openai +## 0.39.2 + +### Patch Changes + +- [`49c5acd`](https://github.com/Effect-TS/effect/commit/49c5acd5932b3d3981353481664606097082ea2f) Thanks @mollyegibson! - Previously, setting `strict: false` on `OpenAiLanguageModel` config caused a 400 "Unknown parameter: 'strict'" response from the OpenAI Responses API, because the flag was spread into the top-level request body instead of being consumed only by the tool and response_format schema builders. The `strict` flag is now stripped from the request body while still controlling `strict` on tool schemas (`prepareTools`) and json_schema response formats (`prepareResponseFormat`). + +## 0.39.1 + +### Patch Changes + +- [`47f0439`](https://github.com/Effect-TS/effect/commit/47f04399d317a1a5619de7dec5b23f6ad7255eca) Thanks @aayushbaluni! - fix(ai-openai): deduplicate response.output items to prevent invalid JSON concatenation + +- [#6187](https://github.com/Effect-TS/effect/pull/6187) [`b63fdb8`](https://github.com/Effect-TS/effect/commit/b63fdb8783a606077ac80d263b4e09b57cdab476) Thanks @alex-dixon! - Change 'in-memory' to 'in_memory' in prompt cache enums + +- [#6174](https://github.com/Effect-TS/effect/pull/6174) [`739f077`](https://github.com/Effect-TS/effect/commit/739f077d1cdfb63d26d4744abd59822231866dac) Thanks @mollyegibson! - Make 'strict' mode configurable for tool definitions passed to the OpenAI model + +- Updated dependencies [[`f99048e`](https://github.com/Effect-TS/effect/commit/f99048e9f4b89ce1afe31e1827dee5d751ddaa5b)]: + - effect@3.21.1 + +## 0.39.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.38.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + ## 0.37.2 ### Patch Changes diff --git a/packages/ai/openai/package.json b/packages/ai/openai/package.json index 7168395e7b3..05b03ed9d41 100644 --- a/packages/ai/openai/package.json +++ b/packages/ai/openai/package.json @@ -1,7 +1,7 @@ { "name": "@effect/ai-openai", "type": "module", - "version": "0.37.2", + "version": "0.39.2", "license": "MIT", "description": "Effect modules for working with AI apis", "homepage": "https://effect.website", diff --git a/packages/ai/openai/src/Generated.ts b/packages/ai/openai/src/Generated.ts index a24c4f35989..e2fd8a325f3 100644 --- a/packages/ai/openai/src/Generated.ts +++ b/packages/ai/openai/src/Generated.ts @@ -2466,7 +2466,7 @@ export class ChatCompletionFunctions extends S.Class("C /** * The retention policy for the prompt cache. Set to `24h` to enable extended prompt caching, which keeps cached prefixes active for longer, up to a maximum of 24 hours. [Learn more](https://platform.openai.com/docs/guides/prompt-caching#prompt-cache-retention). */ -export class CreateChatCompletionRequestPromptCacheRetentionEnum extends S.Literal("in-memory", "24h") {} +export class CreateChatCompletionRequestPromptCacheRetentionEnum extends S.Literal("in_memory", "24h") {} export class CreateChatCompletionRequest extends S.Class("CreateChatCompletionRequest")({ /** @@ -14969,7 +14969,7 @@ export class CreateResponseTruncationEnum extends S.Literal("auto", "disabled") /** * The retention policy for the prompt cache. Set to `24h` to enable extended prompt caching, which keeps cached prefixes active for longer, up to a maximum of 24 hours. [Learn more](https://platform.openai.com/docs/guides/prompt-caching#prompt-cache-retention). */ -export class CreateResponsePromptCacheRetentionEnum extends S.Literal("in-memory", "24h") {} +export class CreateResponsePromptCacheRetentionEnum extends S.Literal("in_memory", "24h") {} export class CreateResponse extends S.Class("CreateResponse")({ "input": S.optionalWith(InputParam, { nullable: true }), @@ -15181,7 +15181,7 @@ export class ResponseTruncationEnum extends S.Literal("auto", "disabled") {} /** * The retention policy for the prompt cache. Set to `24h` to enable extended prompt caching, which keeps cached prefixes active for longer, up to a maximum of 24 hours. [Learn more](https://platform.openai.com/docs/guides/prompt-caching#prompt-cache-retention). */ -export class ResponsePromptCacheRetentionEnum extends S.Literal("in-memory", "24h") {} +export class ResponsePromptCacheRetentionEnum extends S.Literal("in_memory", "24h") {} export class Response extends S.Class("Response")({ /** diff --git a/packages/ai/openai/src/OpenAiLanguageModel.ts b/packages/ai/openai/src/OpenAiLanguageModel.ts index c4fef761467..a2931b95287 100644 --- a/packages/ai/openai/src/OpenAiLanguageModel.ts +++ b/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -94,6 +94,23 @@ export declare namespace Config { */ readonly verbosity?: "low" | "medium" | "high" } + + /** + * Controls whether tool and response format schemas are sent with + * `strict: true` to enable OpenAI's structured outputs mode. + * + * When `true` (default), OpenAI validates that tool schemas comply with + * strict mode requirements (all properties in `required`, + * `additionalProperties: false` on all objects, etc.). + * + * Set to `false` to send tool schemas without strict mode validation, + * which is useful when tool parameter schemas use `Schema.optional()` + * (optional properties are not listed in `required` by Effect's JSON + * schema generator, which strict mode rejects). + * + * Defaults to `true` for backward compatibility. + */ + readonly strict?: boolean } } @@ -283,12 +300,13 @@ export const make = Effect.fnUntraced(function*(options: { const context = yield* Effect.context() const config = { model: options.model, ...options.config, ...context.unsafeMap.get(Config.key) } const messages = yield* prepareMessages(providerOptions, config) - const { toolChoice, tools } = yield* prepareTools(providerOptions) + const { toolChoice, tools } = yield* prepareTools(providerOptions, config) const include = prepareInclude(providerOptions, config) - const responseFormat = prepareResponseFormat(providerOptions) + const responseFormat = prepareResponseFormat(providerOptions, config) const verbosity = config.text?.verbosity + const { strict: _strict, ...requestConfig } = config const request: typeof Generated.CreateResponse.Encoded = { - ...config, + ...requestConfig, input: messages, include, text: { format: responseFormat, verbosity }, @@ -567,7 +585,16 @@ const makeResponse: ( timestamp: DateTime.formatIso(DateTime.unsafeFromDate(createdAt)) }) + // Deduplicate output items by ID to handle OpenAI Responses API bug + // where duplicate OutputMessage items appear in response.output + const seenOutputIds = new Set() for (const part of response.output) { + if (part.id && seenOutputIds.has(part.id)) { + continue + } + if (part.id) { + seenOutputIds.add(part.id) + } switch (part.type) { case "message": { for (const contentPart of part.content) { @@ -1272,10 +1299,10 @@ const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => type OpenAiToolChoice = typeof Generated.CreateResponse.fields.tool_choice.from.Encoded -const prepareTools: (options: LanguageModel.ProviderOptions) => Effect.Effect<{ +const prepareTools: (options: LanguageModel.ProviderOptions, config: Config.Service) => Effect.Effect<{ readonly tools: ReadonlyArray | undefined readonly toolChoice: OpenAiToolChoice | undefined -}, AiError.AiError> = Effect.fnUntraced(function*(options) { +}, AiError.AiError> = Effect.fnUntraced(function*(options, config) { // Return immediately if no tools are in the toolkit if (options.tools.length === 0) { return { tools: undefined, toolChoice: undefined } @@ -1303,7 +1330,7 @@ const prepareTools: (options: LanguageModel.ProviderOptions) => Effect.Effect<{ name: tool.name, description: Tool.getDescription(tool as any), parameters: Tool.getJsonSchema(tool as any) as any, - strict: true + strict: config.strict ?? true }) } @@ -1406,7 +1433,8 @@ const prepareInclude = ( } const prepareResponseFormat = ( - options: LanguageModel.ProviderOptions + options: LanguageModel.ProviderOptions, + config: Config.Service ): typeof Generated.TextResponseFormatConfiguration.Encoded => { if (options.responseFormat.type === "json") { const name = options.responseFormat.objectName @@ -1416,7 +1444,7 @@ const prepareResponseFormat = ( name, description: Tool.getDescriptionFromSchemaAst(schema.ast) ?? "Response with a JSON object", schema: Tool.getJsonSchemaFromSchemaAst(schema.ast) as any, - strict: true + strict: config.strict ?? true } } return { type: "text" } diff --git a/packages/ai/openrouter/CHANGELOG.md b/packages/ai/openrouter/CHANGELOG.md index 30682156b7a..0ad9823ed85 100644 --- a/packages/ai/openrouter/CHANGELOG.md +++ b/packages/ai/openrouter/CHANGELOG.md @@ -1,5 +1,75 @@ # @effect/ai-openrouter +## 0.10.1 + +### Patch Changes + +- [#6145](https://github.com/Effect-TS/effect/pull/6145) [`6c39a34`](https://github.com/Effect-TS/effect/commit/6c39a34c6145811f5c41292f03bf7939cfa8e70d) Thanks @LikiosSedo! - Fix typo in HTTP header name: `HTTP-Referrer` → `HTTP-Referer`. The HTTP spec spells it "Referer" (single r), and OpenRouter expects this exact header name for app attribution. + +- Updated dependencies [[`f99048e`](https://github.com/Effect-TS/effect/commit/f99048e9f4b89ce1afe31e1827dee5d751ddaa5b)]: + - effect@3.21.1 + +## 0.10.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/ai@0.35.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.9.1 + +### Patch Changes + +- [#6131](https://github.com/Effect-TS/effect/pull/6131) [`5c80e57`](https://github.com/Effect-TS/effect/commit/5c80e578bd95e0cf6fceffc72fa0b130ca11ec8e) Thanks @fabstorres! - Allow partial tool_call deltas in OpenRouter streaming + +- Updated dependencies [[`add06f4`](https://github.com/Effect-TS/effect/commit/add06f4521403cbf4b9a692f9b59fb9d3d48293c), [`a03b6a2`](https://github.com/Effect-TS/effect/commit/a03b6a29ed0b983b0440b8ef4be47f47c57d73d7)]: + - effect@3.20.1 + +## 0.9.0 + +### Patch Changes + +- [#6117](https://github.com/Effect-TS/effect/pull/6117) [`7103e24`](https://github.com/Effect-TS/effect/commit/7103e2473db805cc9f0024d4744c77c16d81e2f1) Thanks @nickbreaton! - Fix OpenRouter streaming finalization for usage-only terminal chunks. + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/ai@0.34.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + +## 0.8.4 + +### Patch Changes + +- [#6071](https://github.com/Effect-TS/effect/pull/6071) [`a91364d`](https://github.com/Effect-TS/effect/commit/a91364d0ef48de0f66afe801c4da13bfe8a5aeed) Thanks @marbemac! - Fix `ChatStreamingMessageToolCall` schema rejecting valid streaming tool call chunks. + + The OpenAI streaming spec splits tool calls across multiple SSE chunks — `function.name` is only present on the first chunk, but the schema required it on every chunk, causing a `MalformedOutput` error whenever the model returned a tool call. + + Made `function.name` optional to match `id` which was already optional. + +## 0.8.3 + +### Patch Changes + +- [#6060](https://github.com/Effect-TS/effect/pull/6060) [`c3e706f`](https://github.com/Effect-TS/effect/commit/c3e706ff4d01c70ae1754b13c9cbc1f001c09068) Thanks @nvonbulow! - fix(ai-openrouter): deduplicate reasoning parts when both `reasoning` and `reasoning_details` are present in a stream delta + +- Updated dependencies [[`d67c708`](https://github.com/Effect-TS/effect/commit/d67c7089ba8616b2d48ef7324312267a2a6f310a), [`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f)]: + - @effect/platform@0.94.5 + - effect@3.19.17 + +## 0.8.2 + +### Patch Changes + +- [#6026](https://github.com/Effect-TS/effect/pull/6026) [`38241de`](https://github.com/Effect-TS/effect/commit/38241dee2319d051f3ab15781f73f838d626ac24) Thanks @IMax153! - Fix the OpenRouter AI provider schemas + +- Updated dependencies [[`0023c19`](https://github.com/Effect-TS/effect/commit/0023c19c63c402c050d496817ba92aceea7f25b7), [`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49), [`9a96b87`](https://github.com/Effect-TS/effect/commit/9a96b87a33a75ebc277c585e60758ab4409c0d9e)]: + - @effect/platform@0.94.3 + - effect@3.19.16 + ## 0.8.1 ### Patch Changes diff --git a/packages/ai/openrouter/package.json b/packages/ai/openrouter/package.json index 179a631b2e2..540003dd5e2 100644 --- a/packages/ai/openrouter/package.json +++ b/packages/ai/openrouter/package.json @@ -1,7 +1,7 @@ { "name": "@effect/ai-openrouter", "type": "module", - "version": "0.8.1", + "version": "0.10.1", "license": "MIT", "description": "Effect modules for working with AI apis", "homepage": "https://effect.website", diff --git a/packages/ai/openrouter/scripts/generate.sh b/packages/ai/openrouter/scripts/generate.sh index 163862ee11b..e46f3cea0ac 100755 --- a/packages/ai/openrouter/scripts/generate.sh +++ b/packages/ai/openrouter/scripts/generate.sh @@ -12,7 +12,8 @@ cleanup() { trap cleanup EXIT -openapi_spec_url="https://spec.speakeasy.com/openrouter/sdk/open-router-chat-completions-api-with-code-samples" +openapi_spec_url="https://openrouter.ai/openapi.yaml" +# openapi_spec_url="https://spec.speakeasy.com/openrouter/sdk/open-router-chat-completions-api-with-code-samples" temp_file="${temp_dir}/openrouter.yaml" touch "${temp_file}" diff --git a/packages/ai/openrouter/scripts/generated.patch b/packages/ai/openrouter/scripts/generated.patch index 8119a00d961..bb96a0b3d48 100644 --- a/packages/ai/openrouter/scripts/generated.patch +++ b/packages/ai/openrouter/scripts/generated.patch @@ -1,8 +1,8 @@ diff --git a/packages/ai/openrouter/src/Generated.ts b/packages/ai/openrouter/src/Generated.ts -index 59722a09d..70484d1d7 100644 +index 92a756c..837d7dd 100644 --- a/packages/ai/openrouter/src/Generated.ts +++ b/packages/ai/openrouter/src/Generated.ts -@@ -10,6 +10,118 @@ import * as Effect from "effect/Effect" +@@ -10,6 +10,10 @@ import * as Effect from "effect/Effect" import type { ParseError } from "effect/ParseResult" import * as S from "effect/Schema" @@ -10,75 +10,46 @@ index 59722a09d..70484d1d7 100644 + "type": S.Literal("ephemeral") +}) {} + -+export class ReasoningDetailSummaryType extends S.Literal("reasoning.summary") {} -+ -+export class ReasoningDetailSummaryFormat extends S.Literal("unknown", "openai-responses-v1", "anthropic-claude-v1") {} -+ -+/** -+ * Reasoning summary detail -+ */ + export class OpenResponsesReasoningFormat extends S.Literal( + "unknown", + "openai-responses-v1", +@@ -51,6 +55,70 @@ export class OpenResponsesReasoning extends S.Class("Ope + ) + }) {} + +export class ReasoningDetailSummary extends S.Class("ReasoningDetailSummary")({ -+ "type": ReasoningDetailSummaryType, -+ "summary": S.String, -+ "id": S.optionalWith(S.String, { nullable: true }), -+ "format": S.optionalWith(ReasoningDetailSummaryFormat, { -+ nullable: true, -+ default: () => "anthropic-claude-v1" as const -+ }), -+ "index": S.optionalWith(S.Number, { nullable: true }) ++ id: S.optionalWith(S.String, { nullable: true }), ++ type: S.Literal("reasoning.summary"), ++ index: S.optional(S.Number), ++ format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), ++ summary: S.String +}) {} + -+export class ReasoningDetailEncryptedType extends S.Literal("reasoning.encrypted") {} -+ -+export class ReasoningDetailEncryptedFormat -+ extends S.Literal("unknown", "openai-responses-v1", "anthropic-claude-v1") -+{} -+ -+/** -+ * Encrypted reasoning detail -+ */ +export class ReasoningDetailEncrypted extends S.Class("ReasoningDetailEncrypted")({ -+ "type": ReasoningDetailEncryptedType, -+ "data": S.String, -+ "id": S.optionalWith(S.String, { nullable: true }), -+ "format": S.optionalWith(ReasoningDetailEncryptedFormat, { -+ nullable: true, -+ default: () => "anthropic-claude-v1" as const -+ }), -+ "index": S.optionalWith(S.Number, { nullable: true }) ++ id: S.optionalWith(S.String, { nullable: true }), ++ type: S.Literal("reasoning.encrypted"), ++ index: S.optional(S.Number), ++ format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), ++ data: S.String +}) {} + -+export class ReasoningDetailTextType extends S.Literal("reasoning.text") {} -+ -+export class ReasoningDetailTextFormat extends S.Literal("unknown", "openai-responses-v1", "anthropic-claude-v1") {} -+ -+/** -+ * Text reasoning detail -+ */ +export class ReasoningDetailText extends S.Class("ReasoningDetailText")({ -+ "type": ReasoningDetailTextType, -+ "text": S.optionalWith(S.String, { nullable: true }), -+ "signature": S.optionalWith(S.String, { nullable: true }), -+ "id": S.optionalWith(S.String, { nullable: true }), -+ "format": S.optionalWith(ReasoningDetailTextFormat, { -+ nullable: true, -+ default: () => "anthropic-claude-v1" as const -+ }), -+ "index": S.optionalWith(S.Number, { nullable: true }) ++ id: S.optionalWith(S.String, { nullable: true }), ++ type: S.Literal("reasoning.text"), ++ index: S.optional(S.Number), ++ format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), ++ text: S.optionalWith(S.String, { nullable: true }), ++ signature: S.optionalWith(S.String, { nullable: true }) +}) {} + -+/** -+ * Reasoning detail information -+ */ -+export class ReasoningDetail extends S.Union(ReasoningDetailSummary, ReasoningDetailEncrypted, ReasoningDetailText) {} -+ -+export class FileAnnotationDetailType extends S.Literal("file") {} ++export class ReasoningDetail extends S.Union( ++ ReasoningDetailSummary, ++ ReasoningDetailEncrypted, ++ ReasoningDetailText ++) {} + -+/** -+ * File annotation with content -+ */ +export class FileAnnotationDetail extends S.Class("FileAnnotationDetail")({ -+ "type": FileAnnotationDetailType, ++ "type": S.Literal("file"), + "file": S.Struct({ + "hash": S.String, + "name": S.optionalWith(S.String, { nullable: true }), @@ -97,13 +68,8 @@ index 59722a09d..70484d1d7 100644 + }) +}) {} + -+export class URLCitationAnnotationDetailType extends S.Literal("url_citation") {} -+ -+/** -+ * URL citation annotation -+ */ +export class URLCitationAnnotationDetail extends S.Class("URLCitationAnnotationDetail")({ -+ "type": URLCitationAnnotationDetailType, ++ "type": S.Literal("url_citation"), + "url_citation": S.Struct({ + "end_index": S.Number, + "start_index": S.Number, @@ -113,27 +79,31 @@ index 59722a09d..70484d1d7 100644 + }) +}) {} + -+/** -+ * Annotation information -+ */ +export class AnnotationDetail extends S.Union(FileAnnotationDetail, URLCitationAnnotationDetail) {} + - export class OpenResponsesReasoningFormat - extends S.Literal("unknown", "openai-responses-v1", "xai-responses-v1", "anthropic-claude-v1", "google-gemini-v1") - {} -@@ -2795,7 +2907,10 @@ export class AssistantMessage extends S.Class("AssistantMessag - "name": S.optionalWith(S.String, { nullable: true }), + export class OpenResponsesEasyInputMessageType extends S.Literal("message") {} + + export class OpenResponsesEasyInputMessageRoleEnum extends S.Literal("developer") {} +@@ -4637,7 +4705,7 @@ export class AssistantMessage extends S.Class("AssistantMessag "tool_calls": S.optionalWith(S.Array(ChatMessageToolCall), { nullable: true }), "refusal": S.optionalWith(S.String, { nullable: true }), -- "reasoning": S.optionalWith(S.String, { nullable: true }) -+ "reasoning": S.optionalWith(S.String, { nullable: true }), + "reasoning": S.optionalWith(S.String, { nullable: true }), +- "reasoning_details": S.optionalWith(S.Array(Schema2), { nullable: true }), + "reasoning_details": S.optionalWith(S.Array(ReasoningDetail), { nullable: true }), -+ "images": S.optionalWith(S.Array(ChatMessageContentItemImage), { nullable: true }), + "images": S.optionalWith( + S.Array(S.Struct({ + "image_url": S.Struct({ +@@ -4645,7 +4713,8 @@ export class AssistantMessage extends S.Class("AssistantMessag + }) + })), + { nullable: true } +- ) ++ ), + "annotations": S.optionalWith(S.Array(AnnotationDetail), { nullable: true }) }) {} export class ToolResponseMessage extends S.Class("ToolResponseMessage")({ -@@ -2913,8 +3028,8 @@ export class ChatMessageTokenLogprob extends S.Class("C +@@ -4873,15 +4942,15 @@ export class ChatMessageTokenLogprob extends S.Class("C }) {} export class ChatMessageTokenLogprobs extends S.Class("ChatMessageTokenLogprobs")({ @@ -144,21 +114,25 @@ index 59722a09d..70484d1d7 100644 }) {} export class ChatResponseChoice extends S.Class("ChatResponseChoice")({ -@@ -2928,6 +3043,13 @@ export class ChatGenerationTokenUsage extends S.Class( - "completion_tokens": S.Number, - "prompt_tokens": S.Number, - "total_tokens": S.Number, -+ "cost": S.optionalWith(S.Number, { nullable: true }), -+ "cost_details": S.optionalWith( -+ S.Struct({ -+ upstream_inference_cost: S.optionalWith(S.Number, { nullable: true }) -+ }), -+ { nullable: true } + "finish_reason": S.NullOr(ChatCompletionFinishReason), + "index": S.Number, + "message": AssistantMessage, +- "logprobs": S.optionalWith(ChatMessageTokenLogprobs, { nullable: true }) ++ "logprobs": S.optionalWith(ChatMessageTokenLogprobs, { nullable: true }), + }) {} + + export class ChatGenerationTokenUsage extends S.Class("ChatGenerationTokenUsage")({ +@@ -4905,11 +4974,16 @@ export class ChatGenerationTokenUsage extends S.Class( + "video_tokens": S.optionalWith(S.Number, { nullable: true }) + }), + { nullable: true } +- ) + ), - "completion_tokens_details": S.optionalWith( - S.Struct({ - "reasoning_tokens": S.optionalWith(S.Number, { nullable: true }), -@@ -2949,6 +3071,7 @@ export class ChatGenerationTokenUsage extends S.Class( ++ "cost": S.optionalWith(S.Number, { nullable: true }), ++ "cost_details": S.optionalWith(S.Struct({ upstream_inference_cost: S.optionalWith(S.Number, { nullable: true }) }), { ++ nullable: true ++ }) + }) {} export class ChatResponse extends S.Class("ChatResponse")({ "id": S.String, diff --git a/packages/ai/openrouter/src/Generated.ts b/packages/ai/openrouter/src/Generated.ts index db3f7f37321..5ddbf14e4c1 100644 --- a/packages/ai/openrouter/src/Generated.ts +++ b/packages/ai/openrouter/src/Generated.ts @@ -14,75 +14,80 @@ export class CacheControlEphemeral extends S.Class("Cache "type": S.Literal("ephemeral") }) {} -export class ReasoningDetailSummaryType extends S.Literal("reasoning.summary") {} +export class OpenResponsesReasoningFormat extends S.Literal( + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" +) {} -export class ReasoningDetailSummaryFormat extends S.Literal("unknown", "openai-responses-v1", "anthropic-claude-v1") {} +export class OpenResponsesReasoningType extends S.Literal("reasoning") {} -/** - * Reasoning summary detail - */ -export class ReasoningDetailSummary extends S.Class("ReasoningDetailSummary")({ - "type": ReasoningDetailSummaryType, - "summary": S.String, - "id": S.optionalWith(S.String, { nullable: true }), - "format": S.optionalWith(ReasoningDetailSummaryFormat, { - nullable: true, - default: () => "anthropic-claude-v1" as const - }), - "index": S.optionalWith(S.Number, { nullable: true }) +export class ReasoningTextContentType extends S.Literal("reasoning_text") {} + +export class ReasoningTextContent extends S.Class("ReasoningTextContent")({ + "type": ReasoningTextContentType, + "text": S.String }) {} -export class ReasoningDetailEncryptedType extends S.Literal("reasoning.encrypted") {} +export class ReasoningSummaryTextType extends S.Literal("summary_text") {} -export class ReasoningDetailEncryptedFormat - extends S.Literal("unknown", "openai-responses-v1", "anthropic-claude-v1") -{} +export class ReasoningSummaryText extends S.Class("ReasoningSummaryText")({ + "type": ReasoningSummaryTextType, + "text": S.String +}) {} -/** - * Encrypted reasoning detail - */ -export class ReasoningDetailEncrypted extends S.Class("ReasoningDetailEncrypted")({ - "type": ReasoningDetailEncryptedType, - "data": S.String, - "id": S.optionalWith(S.String, { nullable: true }), - "format": S.optionalWith(ReasoningDetailEncryptedFormat, { - nullable: true, - default: () => "anthropic-claude-v1" as const - }), - "index": S.optionalWith(S.Number, { nullable: true }) +export class OpenResponsesReasoningStatusEnum extends S.Literal("in_progress") {} + +export class OpenResponsesReasoning extends S.Class("OpenResponsesReasoning")({ + "signature": S.optionalWith(S.String, { nullable: true }), + "format": S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + "type": OpenResponsesReasoningType, + "id": S.String, + "content": S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), + "summary": S.Array(ReasoningSummaryText), + "encrypted_content": S.optionalWith(S.String, { nullable: true }), + "status": S.optionalWith( + S.Union(OpenResponsesReasoningStatusEnum, OpenResponsesReasoningStatusEnum, OpenResponsesReasoningStatusEnum), + { nullable: true } + ) }) {} -export class ReasoningDetailTextType extends S.Literal("reasoning.text") {} +export class ReasoningDetailSummary extends S.Class("ReasoningDetailSummary")({ + id: S.optionalWith(S.String, { nullable: true }), + type: S.Literal("reasoning.summary"), + index: S.optional(S.Number), + format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + summary: S.String +}) {} -export class ReasoningDetailTextFormat extends S.Literal("unknown", "openai-responses-v1", "anthropic-claude-v1") {} +export class ReasoningDetailEncrypted extends S.Class("ReasoningDetailEncrypted")({ + id: S.optionalWith(S.String, { nullable: true }), + type: S.Literal("reasoning.encrypted"), + index: S.optional(S.Number), + format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + data: S.String +}) {} -/** - * Text reasoning detail - */ export class ReasoningDetailText extends S.Class("ReasoningDetailText")({ - "type": ReasoningDetailTextType, - "text": S.optionalWith(S.String, { nullable: true }), - "signature": S.optionalWith(S.String, { nullable: true }), - "id": S.optionalWith(S.String, { nullable: true }), - "format": S.optionalWith(ReasoningDetailTextFormat, { - nullable: true, - default: () => "anthropic-claude-v1" as const - }), - "index": S.optionalWith(S.Number, { nullable: true }) + id: S.optionalWith(S.String, { nullable: true }), + type: S.Literal("reasoning.text"), + index: S.optional(S.Number), + format: S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), + text: S.optionalWith(S.String, { nullable: true }), + signature: S.optionalWith(S.String, { nullable: true }) }) {} -/** - * Reasoning detail information - */ -export class ReasoningDetail extends S.Union(ReasoningDetailSummary, ReasoningDetailEncrypted, ReasoningDetailText) {} - -export class FileAnnotationDetailType extends S.Literal("file") {} +export class ReasoningDetail extends S.Union( + ReasoningDetailSummary, + ReasoningDetailEncrypted, + ReasoningDetailText +) {} -/** - * File annotation with content - */ export class FileAnnotationDetail extends S.Class("FileAnnotationDetail")({ - "type": FileAnnotationDetailType, + "type": S.Literal("file"), "file": S.Struct({ "hash": S.String, "name": S.optionalWith(S.String, { nullable: true }), @@ -101,13 +106,8 @@ export class FileAnnotationDetail extends S.Class("FileAnn }) }) {} -export class URLCitationAnnotationDetailType extends S.Literal("url_citation") {} - -/** - * URL citation annotation - */ export class URLCitationAnnotationDetail extends S.Class("URLCitationAnnotationDetail")({ - "type": URLCitationAnnotationDetailType, + "type": S.Literal("url_citation"), "url_citation": S.Struct({ "end_index": S.Number, "start_index": S.Number, @@ -117,47 +117,8 @@ export class URLCitationAnnotationDetail extends S.Class("ReasoningTextContent")({ - "type": ReasoningTextContentType, - "text": S.String -}) {} - -export class ReasoningSummaryTextType extends S.Literal("summary_text") {} - -export class ReasoningSummaryText extends S.Class("ReasoningSummaryText")({ - "type": ReasoningSummaryTextType, - "text": S.String -}) {} - -export class OpenResponsesReasoningStatusEnum extends S.Literal("in_progress") {} - -export class OpenResponsesReasoning extends S.Class("OpenResponsesReasoning")({ - "signature": S.optionalWith(S.String, { nullable: true }), - "format": S.optionalWith(OpenResponsesReasoningFormat, { nullable: true }), - "type": OpenResponsesReasoningType, - "id": S.String, - "content": S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), - "summary": S.Array(ReasoningSummaryText), - "encrypted_content": S.optionalWith(S.String, { nullable: true }), - "status": S.optionalWith( - S.Union(OpenResponsesReasoningStatusEnum, OpenResponsesReasoningStatusEnum, OpenResponsesReasoningStatusEnum), - { nullable: true } - ) -}) {} - export class OpenResponsesEasyInputMessageType extends S.Literal("message") {} export class OpenResponsesEasyInputMessageRoleEnum extends S.Literal("developer") {} @@ -172,19 +133,6 @@ export class ResponseInputText extends S.Class("ResponseInput "text": S.String }) {} -export class ResponseInputImageType extends S.Literal("input_image") {} - -export class ResponseInputImageDetail extends S.Literal("auto", "high", "low") {} - -/** - * Image input content item - */ -export class ResponseInputImage extends S.Class("ResponseInputImage")({ - "type": ResponseInputImageType, - "detail": ResponseInputImageDetail, - "image_url": S.optionalWith(S.String, { nullable: true }) -}) {} - export class ResponseInputFileType extends S.Literal("input_file") {} /** @@ -213,6 +161,19 @@ export class ResponseInputAudio extends S.Class("ResponseInp }) }) {} +export class ResponseInputVideoType extends S.Literal("input_video") {} + +/** + * Video input content item + */ +export class ResponseInputVideo extends S.Class("ResponseInputVideo")({ + "type": ResponseInputVideoType, + /** + * A base64 data URL or remote URL that resolves to a video file + */ + "video_url": S.String +}) {} + export class OpenResponsesEasyInputMessage extends S.Class("OpenResponsesEasyInputMessage")({ "type": S.optionalWith(OpenResponsesEasyInputMessageType, { nullable: true }), @@ -223,7 +184,20 @@ export class OpenResponsesEasyInputMessage OpenResponsesEasyInputMessageRoleEnum ), "content": S.Union( - S.Array(S.Union(ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio)), + S.Array(S.Union( + ResponseInputText, + /** + * Image input content item + */ + S.Struct({ + "type": S.Literal("input_image"), + "detail": S.Literal("auto", "high", "low"), + "image_url": S.optionalWith(S.String, { nullable: true }) + }), + ResponseInputFile, + ResponseInputAudio, + ResponseInputVideo + )), S.String ) }) @@ -242,7 +216,20 @@ export class OpenResponsesInputMessageItem OpenResponsesInputMessageItemRoleEnum, OpenResponsesInputMessageItemRoleEnum ), - "content": S.Array(S.Union(ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio)) + "content": S.Array(S.Union( + ResponseInputText, + /** + * Image input content item + */ + S.Struct({ + "type": S.Literal("input_image"), + "detail": S.Literal("auto", "high", "low"), + "image_url": S.optionalWith(S.String, { nullable: true }) + }), + ResponseInputFile, + ResponseInputAudio, + ResponseInputVideo + )) }) {} @@ -319,7 +306,20 @@ export class OpenAIResponsesAnnotation extends S.Union(FileCitation, URLCitation export class ResponseOutputText extends S.Class("ResponseOutputText")({ "type": ResponseOutputTextType, "text": S.String, - "annotations": S.optionalWith(S.Array(OpenAIResponsesAnnotation), { nullable: true }) + "annotations": S.optionalWith(S.Array(OpenAIResponsesAnnotation), { nullable: true }), + "logprobs": S.optionalWith( + S.Array(S.Struct({ + "token": S.String, + "bytes": S.Array(S.Number), + "logprob": S.Number, + "top_logprobs": S.Array(S.Struct({ + "token": S.String, + "bytes": S.Array(S.Number), + "logprob": S.Number + })) + })), + { nullable: true } + ) }) {} export class OpenAIResponsesRefusalContentType extends S.Literal("refusal") {} @@ -342,12 +342,32 @@ export class ResponsesOutputMessage extends S.Class("Res "content": S.Array(S.Union(ResponseOutputText, OpenAIResponsesRefusalContent)) }) {} +/** + * The format of the reasoning content + */ +export class ResponsesOutputItemReasoningFormat extends S.Literal( + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" +) {} + export class ResponsesOutputItemReasoningType extends S.Literal("reasoning") {} export class ResponsesOutputItemReasoningStatusEnum extends S.Literal("in_progress") {} export class ResponsesOutputItemReasoning extends S.Class("ResponsesOutputItemReasoning")({ + /** + * A signature for the reasoning content, used for verification + */ + "signature": S.optionalWith(S.String, { nullable: true }), + /** + * The format of the reasoning content + */ + "format": S.optionalWith(ResponsesOutputItemReasoningFormat, { nullable: true }), "type": ResponsesOutputItemReasoningType, "id": S.String, "content": S.optionalWith(S.Array(ReasoningTextContent), { nullable: true }), @@ -625,6 +645,8 @@ export class OpenResponsesReasoningConfig }) {} +export class ResponsesOutputModality extends S.Literal("text", "image") {} + export class OpenAIResponsesPrompt extends S.Class("OpenAIResponsesPrompt")({ "id": S.String, "variables": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }) @@ -679,12 +701,12 @@ export class ProviderName extends S.Literal( "Fireworks", "Friendli", "GMICloud", - "GoPomelo", "Google", "Google AI Studio", "Groq", "Hyperbolic", "Inception", + "Inceptron", "InferenceNet", "Infermatic", "Inflection", @@ -709,13 +731,14 @@ export class ProviderName extends S.Literal( "Phala", "Relace", "SambaNova", + "Seed", "SiliconFlow", "Sourceful", "Stealth", "StreamLake", "Switchpoint", - "Targon", "Together", + "Upstage", "Venice", "WandB", "Xiaomi", @@ -740,6 +763,60 @@ export class ProviderSortConfig extends S.Class("ProviderSor */ export class BigNumberUnion extends S.String {} +/** + * Percentile-based throughput cutoffs. All specified cutoffs must be met for an endpoint to be preferred. + */ +export class PercentileThroughputCutoffs extends S.Class("PercentileThroughputCutoffs")({ + /** + * Minimum p50 throughput (tokens/sec) + */ + "p50": S.optionalWith(S.Number, { nullable: true }), + /** + * Minimum p75 throughput (tokens/sec) + */ + "p75": S.optionalWith(S.Number, { nullable: true }), + /** + * Minimum p90 throughput (tokens/sec) + */ + "p90": S.optionalWith(S.Number, { nullable: true }), + /** + * Minimum p99 throughput (tokens/sec) + */ + "p99": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. + */ +export class PreferredMinThroughput extends S.Union(S.Number, PercentileThroughputCutoffs) {} + +/** + * Percentile-based latency cutoffs. All specified cutoffs must be met for an endpoint to be preferred. + */ +export class PercentileLatencyCutoffs extends S.Class("PercentileLatencyCutoffs")({ + /** + * Maximum p50 latency (seconds) + */ + "p50": S.optionalWith(S.Number, { nullable: true }), + /** + * Maximum p75 latency (seconds) + */ + "p75": S.optionalWith(S.Number, { nullable: true }), + /** + * Maximum p90 latency (seconds) + */ + "p90": S.optionalWith(S.Number, { nullable: true }), + /** + * Maximum p99 latency (seconds) + */ + "p99": S.optionalWith(S.Number, { nullable: true }) +}) {} + +/** + * Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. + */ +export class PreferredMaxLatency extends S.Union(S.Number, PercentileLatencyCutoffs) {} + /** * The search engine to use for web search. */ @@ -791,7 +868,23 @@ export class OpenResponsesRequest extends S.Class("OpenRes "max_output_tokens": S.optionalWith(S.Number, { nullable: true }), "temperature": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(2)), { nullable: true }), "top_p": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(0)), { nullable: true }), + "top_logprobs": S.optionalWith(S.Int.pipe(S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(20)), { nullable: true }), + "max_tool_calls": S.optionalWith(S.Int, { nullable: true }), + "presence_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }), + "frequency_penalty": S.optionalWith(S.Number.pipe(S.greaterThanOrEqualTo(-2), S.lessThanOrEqualTo(2)), { + nullable: true + }), "top_k": S.optionalWith(S.Number, { nullable: true }), + /** + * Provider-specific image configuration options. Keys and values vary by model/provider. See https://openrouter.ai/docs/features/multimodal/image-generation for more details. + */ + "image_config": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + /** + * Output modalities for the response. Supported values are "text" and "image". + */ + "modalities": S.optionalWith(S.Array(ResponsesOutputModality), { nullable: true }), "prompt_cache_key": S.optionalWith(S.String, { nullable: true }), "previous_response_id": S.optionalWith(S.String, { nullable: true }), "prompt": S.optionalWith(OpenAIResponsesPrompt, { nullable: true }), @@ -859,22 +952,8 @@ export class OpenResponsesRequest extends S.Class("OpenRes }), { nullable: true } ), - /** - * Preferred minimum throughput (in tokens per second). Endpoints below this threshold may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. - */ - "preferred_min_throughput": S.optionalWith(S.Number, { nullable: true }), - /** - * Preferred maximum latency (in seconds). Endpoints above this threshold may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. - */ - "preferred_max_latency": S.optionalWith(S.Number, { nullable: true }), - /** - * **DEPRECATED** Use preferred_min_throughput instead. Backwards-compatible alias for preferred_min_throughput. - */ - "min_throughput": S.optionalWith(S.Number, { nullable: true }), - /** - * **DEPRECATED** Use preferred_max_latency instead. Backwards-compatible alias for preferred_max_latency. - */ - "max_latency": S.optionalWith(S.Number, { nullable: true }) + "preferred_min_throughput": S.optionalWith(PreferredMinThroughput, { nullable: true }), + "preferred_max_latency": S.optionalWith(PreferredMaxLatency, { nullable: true }) }), { nullable: true } ), @@ -883,6 +962,17 @@ export class OpenResponsesRequest extends S.Class("OpenRes */ "plugins": S.optionalWith( S.Array(S.Union( + S.Struct({ + "id": S.Literal("auto-router"), + /** + * Set to false to disable the auto-router plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + /** + * List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }) + }), S.Struct({ "id": S.Literal("moderation") }), @@ -1059,6 +1149,19 @@ export class OpenAIResponsesIncompleteDetails }) {} +export class ResponseInputImageType extends S.Literal("input_image") {} + +export class ResponseInputImageDetail extends S.Literal("auto", "high", "low") {} + +/** + * Image input content item + */ +export class ResponseInputImage extends S.Class("ResponseInputImage")({ + "type": ResponseInputImageType, + "detail": ResponseInputImageDetail, + "image_url": S.optionalWith(S.String, { nullable: true }) +}) {} + export class OpenAIResponsesInput extends S.Union( S.String, S.Array(S.Union( @@ -1134,7 +1237,8 @@ export class OpenResponsesNonStreamingResponse "object": OpenResponsesNonStreamingResponseObject, "created_at": S.Number, "model": S.String, - "status": S.optionalWith(OpenAIResponsesResponseStatus, { nullable: true }), + "status": OpenAIResponsesResponseStatus, + "completed_at": S.NullOr(S.Number), "user": S.optionalWith(S.String, { nullable: true }), "output_text": S.optionalWith(S.String, { nullable: true }), "prompt_cache_key": S.optionalWith(S.String, { nullable: true }), @@ -1146,6 +1250,8 @@ export class OpenResponsesNonStreamingResponse "max_output_tokens": S.optionalWith(S.Number, { nullable: true }), "temperature": S.NullOr(S.Number), "top_p": S.NullOr(S.Number), + "presence_penalty": S.NullOr(S.Number), + "frequency_penalty": S.NullOr(S.Number), "instructions": OpenAIResponsesInput, "metadata": S.NullOr(OpenResponsesRequestMetadata), "tools": S.Array(S.Union( @@ -1412,59 +1518,1035 @@ export class ProviderOverloadedResponse extends S.Class("ActivityItem")({ - /** - * Date of the activity (YYYY-MM-DD format) - */ - "date": S.String, - /** - * Model slug (e.g., "openai/gpt-4.1") - */ - "model": S.String, - /** - * Model permaslug (e.g., "openai/gpt-4.1-2025-04-14") - */ - "model_permaslug": S.String, - /** - * Unique identifier for the endpoint - */ - "endpoint_id": S.String, - /** - * Name of the provider serving this endpoint - */ - "provider_name": S.String, - /** - * Total cost in USD (OpenRouter credits spent) - */ - "usage": S.Number, - /** - * BYOK inference cost in USD (external credits spent) - */ - "byok_usage_inference": S.Number, - /** - * Number of requests made - */ - "requests": S.Number, - /** - * Total prompt tokens used - */ - "prompt_tokens": S.Number, - /** - * Total completion tokens generated - */ - "completion_tokens": S.Number, - /** - * Total reasoning tokens used - */ - "reasoning_tokens": S.Number -}) {} +/** + * Anthropic message with OpenRouter extensions + */ +export class OpenRouterAnthropicMessageParam + extends S.Class("OpenRouterAnthropicMessageParam")({ + "role": OpenRouterAnthropicMessageParamRole, + "content": S.Union( + S.String, + S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("image"), + "source": S.Union( + S.Struct({ + "type": S.Literal("base64"), + "media_type": S.Literal("image/jpeg", "image/png", "image/gif", "image/webp"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("url"), + "url": S.String + }) + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("document"), + "source": S.Union( + S.Struct({ + "type": S.Literal("base64"), + "media_type": S.Literal("application/pdf"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("text"), + "media_type": S.Literal("text/plain"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("content"), + "content": S.Union( + S.String, + S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("image"), + "source": S.Union( + S.Struct({ + "type": S.Literal("base64"), + "media_type": S.Literal("image/jpeg", "image/png", "image/gif", "image/webp"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("url"), + "url": S.String + }) + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }) + )) + ) + }), + S.Struct({ + "type": S.Literal("url"), + "url": S.String + }) + ), + "citations": S.optionalWith( + S.Struct({ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) + }), + { nullable: true } + ), + "context": S.optionalWith(S.String, { nullable: true }), + "title": S.optionalWith(S.String, { nullable: true }), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("tool_use"), + "id": S.String, + "name": S.String, + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("tool_result"), + "tool_use_id": S.String, + "content": S.optionalWith( + S.Union( + S.String, + S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("image"), + "source": S.Union( + S.Struct({ + "type": S.Literal("base64"), + "media_type": S.Literal("image/jpeg", "image/png", "image/gif", "image/webp"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("url"), + "url": S.String + }) + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }) + )) + ), + { nullable: true } + ), + "is_error": S.optionalWith(S.Boolean, { nullable: true }), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("thinking"), + "thinking": S.String, + "signature": S.String + }), + S.Struct({ + "type": S.Literal("redacted_thinking"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("server_tool_use"), + "id": S.String, + "name": S.Literal("web_search"), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("web_search_tool_result"), + "tool_use_id": S.String, + "content": S.Union( + S.Array(S.Struct({ + "type": S.Literal("web_search_result"), + "encrypted_content": S.String, + "title": S.String, + "url": S.String, + "page_age": S.optionalWith(S.String, { nullable: true }) + })), + S.Struct({ + "type": S.Literal("web_search_tool_result_error"), + "error_code": S.Literal( + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ) + }) + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("search_result"), + "source": S.String, + "title": S.String, + "content": S.Array(S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + })), + "citations": S.optionalWith( + S.Struct({ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) + }), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }) + )) + ) + }) +{} + +export class AnthropicMessagesRequestToolChoiceEnumType extends S.Literal("tool") {} + +export class AnthropicMessagesRequestThinkingEnumType extends S.Literal("disabled") {} + +export class AnthropicMessagesRequestServiceTier extends S.Literal("auto", "standard_only") {} + +/** + * The sorting strategy to use for this request, if "order" is not specified. When set, no load balancing is performed. + */ +export class AnthropicMessagesRequestProviderSort extends S.Literal("price", "throughput", "latency") {} + +/** + * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). + */ +export class AnthropicMessagesRequestRoute extends S.Literal("fallback", "sort") {} + +/** + * Request schema for Anthropic Messages API endpoint + */ +export class AnthropicMessagesRequest extends S.Class("AnthropicMessagesRequest")({ + "model": S.String, + "max_tokens": S.Number, + "messages": S.Array(OpenRouterAnthropicMessageParam), + "system": S.optionalWith( + S.Union( + S.String, + S.Array(S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + )), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + })) + ), + { nullable: true } + ), + "metadata": S.optionalWith( + S.Struct({ + "user_id": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + "stop_sequences": S.optionalWith(S.Array(S.String), { nullable: true }), + "stream": S.optionalWith(S.Boolean, { nullable: true }), + "temperature": S.optionalWith(S.Number, { nullable: true }), + "top_p": S.optionalWith(S.Number, { nullable: true }), + "top_k": S.optionalWith(S.Number, { nullable: true }), + "tools": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "name": S.String, + "description": S.optionalWith(S.String, { nullable: true }), + "input_schema": S.Struct({ + "type": S.Literal("object"), + "required": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + "type": S.optionalWith(S.Literal("custom"), { nullable: true }), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("bash_20250124"), + "name": S.Literal("bash"), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("text_editor_20250124"), + "name": S.Literal("str_replace_editor"), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }), + S.Struct({ + "type": S.Literal("web_search_20250305"), + "name": S.Literal("web_search"), + "allowed_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + "blocked_domains": S.optionalWith(S.Array(S.String), { nullable: true }), + "max_uses": S.optionalWith(S.Number, { nullable: true }), + "user_location": S.optionalWith( + S.Struct({ + "type": S.Literal("approximate"), + "city": S.optionalWith(S.String, { nullable: true }), + "country": S.optionalWith(S.String, { nullable: true }), + "region": S.optionalWith(S.String, { nullable: true }), + "timezone": S.optionalWith(S.String, { nullable: true }) + }), + { nullable: true } + ), + "cache_control": S.optionalWith( + S.Struct({ + "type": S.Literal("ephemeral"), + "ttl": S.optionalWith(S.Literal("5m", "1h"), { nullable: true }) + }), + { nullable: true } + ) + }) + )), + { nullable: true } + ), + "tool_choice": S.optionalWith( + S.Union( + S.Struct({ + "type": AnthropicMessagesRequestToolChoiceEnumType, + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }) + }), + S.Struct({ + "type": AnthropicMessagesRequestToolChoiceEnumType, + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }) + }), + S.Struct({ + "type": AnthropicMessagesRequestToolChoiceEnumType + }), + S.Struct({ + "type": AnthropicMessagesRequestToolChoiceEnumType, + "name": S.String, + "disable_parallel_tool_use": S.optionalWith(S.Boolean, { nullable: true }) + }) + ), + { nullable: true } + ), + "thinking": S.optionalWith( + S.Union( + S.Struct({ + "type": AnthropicMessagesRequestThinkingEnumType, + "budget_tokens": S.Number + }), + S.Struct({ + "type": AnthropicMessagesRequestThinkingEnumType + }) + ), + { nullable: true } + ), + "service_tier": S.optionalWith(AnthropicMessagesRequestServiceTier, { nullable: true }), + /** + * When multiple model providers are available, optionally indicate your routing preference. + */ + "provider": S.optionalWith( + S.Struct({ + /** + * Whether to allow backup providers to serve requests + * - true: (default) when the primary provider (or your custom providers in "order") is unavailable, use the next best provider. + * - false: use only the primary/custom provider, and return the upstream error if it's unavailable. + */ + "allow_fallbacks": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest. + */ + "require_parameters": S.optionalWith(S.Boolean, { nullable: true }), + "data_collection": S.optionalWith(DataCollection, { nullable: true }), + /** + * Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used. + */ + "zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used. + */ + "enforce_distillable_text": S.optionalWith(S.Boolean, { nullable: true }), + /** + * An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message. + */ + "order": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request. + */ + "only": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request. + */ + "ignore": S.optionalWith(S.Array(S.Union(ProviderName, S.String)), { nullable: true }), + /** + * A list of quantization levels to filter the provider by. + */ + "quantizations": S.optionalWith(S.Array(Quantization), { nullable: true }), + "sort": S.optionalWith(AnthropicMessagesRequestProviderSort, { nullable: true }), + /** + * The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion. + */ + "max_price": S.optionalWith( + S.Struct({ + "prompt": S.optionalWith(BigNumberUnion, { nullable: true }), + "completion": S.optionalWith(BigNumberUnion, { nullable: true }), + "image": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio": S.optionalWith(BigNumberUnion, { nullable: true }), + "request": S.optionalWith(BigNumberUnion, { nullable: true }) + }), + { nullable: true } + ), + "preferred_min_throughput": S.optionalWith(PreferredMinThroughput, { nullable: true }), + "preferred_max_latency": S.optionalWith(PreferredMaxLatency, { nullable: true }) + }), + { nullable: true } + ), + /** + * Plugins you want to enable for this request, including their settings. + */ + "plugins": S.optionalWith( + S.Array(S.Union( + S.Struct({ + "id": S.Literal("auto-router"), + /** + * Set to false to disable the auto-router plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + /** + * List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., "anthropic/*" matches all Anthropic models). When not specified, uses the default supported models list. + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }) + }), + S.Struct({ + "id": S.Literal("moderation") + }), + S.Struct({ + "id": S.Literal("web"), + /** + * Set to false to disable the web-search plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + "max_results": S.optionalWith(S.Number, { nullable: true }), + "search_prompt": S.optionalWith(S.String, { nullable: true }), + "engine": S.optionalWith(WebSearchEngine, { nullable: true }) + }), + S.Struct({ + "id": S.Literal("file-parser"), + /** + * Set to false to disable the file-parser plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }), + "pdf": S.optionalWith(PDFParserOptions, { nullable: true }) + }), + S.Struct({ + "id": S.Literal("response-healing"), + /** + * Set to false to disable the response-healing plugin for this request. Defaults to true. + */ + "enabled": S.optionalWith(S.Boolean, { nullable: true }) + }) + )), + { nullable: true } + ), + /** + * **DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: "fallback" (maps to "model"), "sort" (maps to "none"). + */ + "route": S.optionalWith(AnthropicMessagesRequestRoute, { nullable: true }), + /** + * A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters. + */ + "user": S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + /** + * A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters. + */ + "session_id": S.optionalWith(S.String.pipe(S.maxLength(128)), { nullable: true }), + "models": S.optionalWith(S.Array(S.String), { nullable: true }) +}) {} + +export class AnthropicMessagesResponseType extends S.Literal("message") {} + +export class AnthropicMessagesResponseRole extends S.Literal("assistant") {} + +export class AnthropicMessagesResponseStopReason extends S.Literal( + "end_turn", + "max_tokens", + "stop_sequence", + "tool_use", + "pause_turn", + "refusal", + "model_context_window_exceeded" +) {} + +export class AnthropicMessagesResponseUsageServiceTier extends S.Literal("standard", "priority", "batch") {} + +export class AnthropicMessagesResponse extends S.Class("AnthropicMessagesResponse")({ + "id": S.String, + "type": AnthropicMessagesResponseType, + "role": AnthropicMessagesResponseRole, + "content": S.Array(S.Union( + S.Struct({ + "type": S.Literal("text"), + "text": S.String, + "citations": S.NullOr(S.Array(S.Union( + S.Struct({ + "type": S.Literal("char_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_char_index": S.Number, + "end_char_index": S.Number, + "file_id": S.NullOr(S.String) + }), + S.Struct({ + "type": S.Literal("page_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_page_number": S.Number, + "end_page_number": S.Number, + "file_id": S.NullOr(S.String) + }), + S.Struct({ + "type": S.Literal("content_block_location"), + "cited_text": S.String, + "document_index": S.Number, + "document_title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number, + "file_id": S.NullOr(S.String) + }), + S.Struct({ + "type": S.Literal("web_search_result_location"), + "cited_text": S.String, + "encrypted_index": S.String, + "title": S.NullOr(S.String), + "url": S.String + }), + S.Struct({ + "type": S.Literal("search_result_location"), + "cited_text": S.String, + "search_result_index": S.Number, + "source": S.String, + "title": S.NullOr(S.String), + "start_block_index": S.Number, + "end_block_index": S.Number + }) + ))) + }), + S.Struct({ + "type": S.Literal("tool_use"), + "id": S.String, + "name": S.String + }), + S.Struct({ + "type": S.Literal("thinking"), + "thinking": S.String, + "signature": S.String + }), + S.Struct({ + "type": S.Literal("redacted_thinking"), + "data": S.String + }), + S.Struct({ + "type": S.Literal("server_tool_use"), + "id": S.String, + "name": S.Literal("web_search") + }), + S.Struct({ + "type": S.Literal("web_search_tool_result"), + "tool_use_id": S.String, + "content": S.Union( + S.Array(S.Struct({ + "type": S.Literal("web_search_result"), + "encrypted_content": S.String, + "page_age": S.NullOr(S.String), + "title": S.String, + "url": S.String + })), + S.Struct({ + "type": S.Literal("web_search_tool_result_error"), + "error_code": S.Literal( + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ) + }) + ) + }) + )), + "model": S.String, + "stop_reason": S.NullOr(AnthropicMessagesResponseStopReason), + "stop_sequence": S.NullOr(S.String), + "usage": S.Struct({ + "input_tokens": S.Number, + "output_tokens": S.Number, + "cache_creation_input_tokens": S.NullOr(S.Number), + "cache_read_input_tokens": S.NullOr(S.Number), + "cache_creation": S.NullOr(S.Struct({ + "ephemeral_5m_input_tokens": S.Number, + "ephemeral_1h_input_tokens": S.Number + })), + "server_tool_use": S.NullOr(S.Struct({ + "web_search_requests": S.Number + })), + "service_tier": S.NullOr(AnthropicMessagesResponseUsageServiceTier) + }) +}) {} + +export class CreateMessages400Type extends S.Literal("error") {} + +export class CreateMessages400 extends S.Struct({ + "type": CreateMessages400Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages401Type extends S.Literal("error") {} + +export class CreateMessages401 extends S.Struct({ + "type": CreateMessages401Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages403Type extends S.Literal("error") {} + +export class CreateMessages403 extends S.Struct({ + "type": CreateMessages403Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages404Type extends S.Literal("error") {} + +export class CreateMessages404 extends S.Struct({ + "type": CreateMessages404Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages429Type extends S.Literal("error") {} + +export class CreateMessages429 extends S.Struct({ + "type": CreateMessages429Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages500Type extends S.Literal("error") {} + +export class CreateMessages500 extends S.Struct({ + "type": CreateMessages500Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages503Type extends S.Literal("error") {} + +export class CreateMessages503 extends S.Struct({ + "type": CreateMessages503Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class CreateMessages529Type extends S.Literal("error") {} + +export class CreateMessages529 extends S.Struct({ + "type": CreateMessages529Type, + "error": S.Struct({ + "type": S.String, + "message": S.String + }) +}) {} + +export class GetUserActivityParams extends S.Struct({ + /** + * Filter by a single UTC date in the last 30 days (YYYY-MM-DD format). + */ + "date": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ActivityItem extends S.Class("ActivityItem")({ + /** + * Date of the activity (YYYY-MM-DD format) + */ + "date": S.String, + /** + * Model slug (e.g., "openai/gpt-4.1") + */ + "model": S.String, + /** + * Model permaslug (e.g., "openai/gpt-4.1-2025-04-14") + */ + "model_permaslug": S.String, + /** + * Unique identifier for the endpoint + */ + "endpoint_id": S.String, + /** + * Name of the provider serving this endpoint + */ + "provider_name": S.String, + /** + * Total cost in USD (OpenRouter credits spent) + */ + "usage": S.Number, + /** + * BYOK inference cost in USD (external credits spent) + */ + "byok_usage_inference": S.Number, + /** + * Number of requests made + */ + "requests": S.Number, + /** + * Total prompt tokens used + */ + "prompt_tokens": S.Number, + /** + * Total completion tokens generated + */ + "completion_tokens": S.Number, + /** + * Total reasoning tokens used + */ + "reasoning_tokens": S.Number +}) {} export class GetUserActivity200 extends S.Struct({ /** @@ -1606,22 +2688,8 @@ export class ProviderPreferences extends S.Class("ProviderP }), { nullable: true } ), - /** - * Preferred minimum throughput (in tokens per second). Endpoints below this threshold may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. - */ - "preferred_min_throughput": S.optionalWith(S.Number, { nullable: true }), - /** - * Preferred maximum latency (in seconds). Endpoints above this threshold may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. - */ - "preferred_max_latency": S.optionalWith(S.Number, { nullable: true }), - /** - * **DEPRECATED** Use preferred_min_throughput instead. Backwards-compatible alias for preferred_min_throughput. - */ - "min_throughput": S.optionalWith(S.Number, { nullable: true }), - /** - * **DEPRECATED** Use preferred_max_latency instead. Backwards-compatible alias for preferred_max_latency. - */ - "max_latency": S.optionalWith(S.Number, { nullable: true }) + "preferred_min_throughput": S.optionalWith(PreferredMinThroughput, { nullable: true }), + "preferred_max_latency": S.optionalWith(PreferredMaxLatency, { nullable: true }) }) {} export class CreateEmbeddingsRequest extends S.Class("CreateEmbeddingsRequest")({ @@ -1685,6 +2753,7 @@ export class PublicPricing extends S.Class("PublicPricing")({ "image_token": S.optionalWith(BigNumberUnion, { nullable: true }), "image_output": S.optionalWith(BigNumberUnion, { nullable: true }), "audio": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio_output": S.optionalWith(BigNumberUnion, { nullable: true }), "input_audio_cache": S.optionalWith(BigNumberUnion, { nullable: true }), "web_search": S.optionalWith(BigNumberUnion, { nullable: true }), "internal_reasoning": S.optionalWith(BigNumberUnion, { nullable: true }), @@ -1748,7 +2817,7 @@ export class ModelArchitectureInstructType extends S.Literal( export class InputModality extends S.Literal("text", "image", "file", "audio", "video") {} -export class OutputModality extends S.Literal("text", "image", "embeddings") {} +export class OutputModality extends S.Literal("text", "image", "embeddings", "audio") {} /** * Model architecture information @@ -1883,7 +2952,11 @@ export class Model extends S.Class("Model")({ * List of supported parameters for this model */ "supported_parameters": S.Array(Parameter), - "default_parameters": S.NullOr(DefaultParameters) + "default_parameters": S.NullOr(DefaultParameters), + /** + * The date after which the model may be removed. ISO 8601 date string (YYYY-MM-DD) or null if no expiration. + */ + "expiration_date": S.optionalWith(S.String, { nullable: true }) }) {} /** @@ -2042,7 +3115,11 @@ export class GetGeneration200 extends S.Struct({ /** * Type of API used for the generation */ - "api_type": S.NullOr(GetGeneration200DataApiType) + "api_type": S.NullOr(GetGeneration200DataApiType), + /** + * Router used for the request (e.g., openrouter/auto) + */ + "router": S.NullOr(S.String) }) }) {} @@ -2061,8 +3138,29 @@ export class ModelsCountResponse extends S.Class("ModelsCou }) }) {} +/** + * Filter models by use case category + */ +export class GetModelsParamsCategory extends S.Literal( + "programming", + "roleplay", + "marketing", + "marketing/seo", + "technology", + "science", + "translation", + "legal", + "finance", + "health", + "trivia", + "academia" +) {} + export class GetModelsParams extends S.Struct({ - "category": S.optionalWith(S.String, { nullable: true }), + /** + * Filter models by use case category + */ + "category": S.optionalWith(GetModelsParamsCategory, { nullable: true }), "supported_parameters": S.optionalWith(S.String, { nullable: true }) }) {} @@ -2150,11 +3248,59 @@ export class PublicEndpointQuantization extends PublicEndpointQuantizationEnum { export class EndpointStatus extends S.Literal(0, -1, -2, -3, -5, -10) {} +/** + * Latency percentiles in milliseconds over the last 30 minutes. Latency measures time to first token. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests. + */ +export class PercentileStats extends S.Class("PercentileStats")({ + /** + * Median (50th percentile) + */ + "p50": S.Number, + /** + * 75th percentile + */ + "p75": S.Number, + /** + * 90th percentile + */ + "p90": S.Number, + /** + * 99th percentile + */ + "p99": S.Number +}) {} + +/** + * Throughput percentiles in tokens per second over the last 30 minutes. Throughput measures output token generation speed. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests. + */ +export class PublicEndpointThroughputLast30M extends S.Struct({ + /** + * Median (50th percentile) + */ + "p50": S.Number, + /** + * 75th percentile + */ + "p75": S.Number, + /** + * 90th percentile + */ + "p90": S.Number, + /** + * 99th percentile + */ + "p99": S.Number +}) {} + /** * Information about a specific model endpoint */ export class PublicEndpoint extends S.Class("PublicEndpoint")({ "name": S.String, + /** + * The unique identifier for the model (permaslug) + */ + "model_id": S.String, "model_name": S.String, "context_length": S.Number, "pricing": S.Struct({ @@ -2165,6 +3311,7 @@ export class PublicEndpoint extends S.Class("PublicEndpoint")({ "image_token": S.optionalWith(BigNumberUnion, { nullable: true }), "image_output": S.optionalWith(BigNumberUnion, { nullable: true }), "audio": S.optionalWith(BigNumberUnion, { nullable: true }), + "audio_output": S.optionalWith(BigNumberUnion, { nullable: true }), "input_audio_cache": S.optionalWith(BigNumberUnion, { nullable: true }), "web_search": S.optionalWith(BigNumberUnion, { nullable: true }), "internal_reasoning": S.optionalWith(BigNumberUnion, { nullable: true }), @@ -2180,7 +3327,9 @@ export class PublicEndpoint extends S.Class("PublicEndpoint")({ "supported_parameters": S.Array(Parameter), "status": S.optionalWith(EndpointStatus, { nullable: true }), "uptime_last_30m": S.NullOr(S.Number), - "supports_implicit_caching": S.Boolean + "supports_implicit_caching": S.Boolean, + "latency_last_30m": S.NullOr(PercentileStats), + "throughput_last_30m": PublicEndpointThroughputLast30M }) {} /** @@ -2218,53 +3367,6 @@ export class ListEndpointsZdr200 extends S.Struct({ "data": S.Array(PublicEndpoint) }) {} -export class GetParametersParams extends S.Struct({ - "provider": S.optionalWith(ProviderName, { nullable: true }) -}) {} - -export class GetParameters200 extends S.Struct({ - /** - * Parameter analytics data - */ - "data": S.Struct({ - /** - * Model identifier - */ - "model": S.String, - /** - * List of parameters supported by this model - */ - "supported_parameters": S.Array( - S.Literal( - "temperature", - "top_p", - "top_k", - "min_p", - "top_a", - "frequency_penalty", - "presence_penalty", - "repetition_penalty", - "max_tokens", - "logit_bias", - "logprobs", - "top_logprobs", - "seed", - "response_format", - "structured_outputs", - "stop", - "tools", - "tool_choice", - "parallel_tool_calls", - "include_reasoning", - "reasoning", - "reasoning_effort", - "web_search_options", - "verbosity" - ) - ) - }) -}) {} - export class ListProviders200 extends S.Struct({ "data": S.Array(S.Struct({ /** @@ -2286,26 +3388,226 @@ export class ListProviders200 extends S.Struct({ /** * URL to the provider's status page */ - "status_page_url": S.optionalWith(S.String, { nullable: true }) - })) -}) {} - -export class ListParams extends S.Struct({ - /** - * Whether to include disabled API keys in the response - */ - "include_disabled": S.optionalWith(S.String, { nullable: true }), + "status_page_url": S.optionalWith(S.String, { nullable: true }) + })) +}) {} + +export class ListParams extends S.Struct({ + /** + * Whether to include disabled API keys in the response + */ + "include_disabled": S.optionalWith(S.String, { nullable: true }), + /** + * Number of API keys to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class List200 extends S.Struct({ + /** + * List of API keys + */ + "data": S.Array(S.Struct({ + /** + * Unique hash identifier for the API key + */ + "hash": S.String, + /** + * Name of the API key + */ + "name": S.String, + /** + * Human-readable label for the API key + */ + "label": S.String, + /** + * Whether the API key is disabled + */ + "disabled": S.Boolean, + /** + * Spending limit for the API key in USD + */ + "limit": S.NullOr(S.Number), + /** + * Remaining spending limit in USD + */ + "limit_remaining": S.NullOr(S.Number), + /** + * Type of limit reset for the API key + */ + "limit_reset": S.NullOr(S.String), + /** + * Whether to include external BYOK usage in the credit limit + */ + "include_byok_in_limit": S.Boolean, + /** + * Total OpenRouter credit usage (in USD) for the API key + */ + "usage": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC day + */ + "usage_daily": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + */ + "usage_weekly": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC month + */ + "usage_monthly": S.Number, + /** + * Total external BYOK usage (in USD) for the API key + */ + "byok_usage": S.Number, + /** + * External BYOK usage (in USD) for the current UTC day + */ + "byok_usage_daily": S.Number, + /** + * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + */ + "byok_usage_weekly": S.Number, + /** + * External BYOK usage (in USD) for current UTC month + */ + "byok_usage_monthly": S.Number, + /** + * ISO 8601 timestamp of when the API key was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the API key was last updated + */ + "updated_at": S.NullOr(S.String), + /** + * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) + })) +}) {} + +/** + * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + */ +export class CreateKeysRequestLimitReset extends S.Literal("daily", "weekly", "monthly") {} + +export class CreateKeysRequest extends S.Class("CreateKeysRequest")({ + /** + * Name for the new API key + */ + "name": S.String.pipe(S.minLength(1)), + /** + * Optional spending limit for the API key in USD + */ + "limit": S.optionalWith(S.Number, { nullable: true }), + /** + * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + */ + "limit_reset": S.optionalWith(CreateKeysRequestLimitReset, { nullable: true }), + /** + * Whether to include BYOK usage in the limit + */ + "include_byok_in_limit": S.optionalWith(S.Boolean, { nullable: true }), + /** + * Optional ISO 8601 UTC timestamp when the API key should expire. Must be UTC, other timezones will be rejected + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class CreateKeys201 extends S.Struct({ + /** + * The created API key information + */ + "data": S.Struct({ + /** + * Unique hash identifier for the API key + */ + "hash": S.String, + /** + * Name of the API key + */ + "name": S.String, + /** + * Human-readable label for the API key + */ + "label": S.String, + /** + * Whether the API key is disabled + */ + "disabled": S.Boolean, + /** + * Spending limit for the API key in USD + */ + "limit": S.NullOr(S.Number), + /** + * Remaining spending limit in USD + */ + "limit_remaining": S.NullOr(S.Number), + /** + * Type of limit reset for the API key + */ + "limit_reset": S.NullOr(S.String), + /** + * Whether to include external BYOK usage in the credit limit + */ + "include_byok_in_limit": S.Boolean, + /** + * Total OpenRouter credit usage (in USD) for the API key + */ + "usage": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC day + */ + "usage_daily": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + */ + "usage_weekly": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC month + */ + "usage_monthly": S.Number, + /** + * Total external BYOK usage (in USD) for the API key + */ + "byok_usage": S.Number, + /** + * External BYOK usage (in USD) for the current UTC day + */ + "byok_usage_daily": S.Number, + /** + * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + */ + "byok_usage_weekly": S.Number, + /** + * External BYOK usage (in USD) for current UTC month + */ + "byok_usage_monthly": S.Number, + /** + * ISO 8601 timestamp of when the API key was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the API key was last updated + */ + "updated_at": S.NullOr(S.String), + /** + * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) + }), /** - * Number of API keys to skip for pagination + * The actual API key string (only shown once) */ - "offset": S.optionalWith(S.String, { nullable: true }) + "key": S.String }) {} -export class List200 extends S.Struct({ +export class GetKey200 extends S.Struct({ /** - * List of API keys + * The API key information */ - "data": S.Array(S.Struct({ + "data": S.Struct({ /** * Unique hash identifier for the API key */ @@ -2382,40 +3684,47 @@ export class List200 extends S.Struct({ * ISO 8601 UTC timestamp when the API key expires, or null if no expiration */ "expires_at": S.optionalWith(S.String, { nullable: true }) - })) + }) +}) {} + +export class DeleteKeys200 extends S.Struct({ + /** + * Confirmation that the API key was deleted + */ + "deleted": S.Literal(true) }) {} /** - * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. */ -export class CreateKeysRequestLimitReset extends S.Literal("daily", "weekly", "monthly") {} +export class UpdateKeysRequestLimitReset extends S.Literal("daily", "weekly", "monthly") {} -export class CreateKeysRequest extends S.Class("CreateKeysRequest")({ +export class UpdateKeysRequest extends S.Class("UpdateKeysRequest")({ /** - * Name for the new API key + * New name for the API key */ - "name": S.String.pipe(S.minLength(1)), + "name": S.optionalWith(S.String, { nullable: true }), /** - * Optional spending limit for the API key in USD + * Whether to disable the API key */ - "limit": S.optionalWith(S.Number, { nullable: true }), + "disabled": S.optionalWith(S.Boolean, { nullable: true }), /** - * Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + * New spending limit for the API key in USD */ - "limit_reset": S.optionalWith(CreateKeysRequestLimitReset, { nullable: true }), + "limit": S.optionalWith(S.Number, { nullable: true }), /** - * Whether to include BYOK usage in the limit + * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. */ - "include_byok_in_limit": S.optionalWith(S.Boolean, { nullable: true }), + "limit_reset": S.optionalWith(UpdateKeysRequestLimitReset, { nullable: true }), /** - * Optional ISO 8601 UTC timestamp when the API key should expire. Must be UTC, other timezones will be rejected + * Whether to include BYOK usage in the limit */ - "expires_at": S.optionalWith(S.String, { nullable: true }) + "include_byok_in_limit": S.optionalWith(S.Boolean, { nullable: true }) }) {} -export class CreateKeys201 extends S.Struct({ +export class UpdateKeys200 extends S.Struct({ /** - * The created API key information + * The updated API key information */ "data": S.Struct({ /** @@ -2457,251 +3766,605 @@ export class CreateKeys201 extends S.Struct({ /** * OpenRouter credit usage (in USD) for the current UTC day */ - "usage_daily": S.Number, + "usage_daily": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + */ + "usage_weekly": S.Number, + /** + * OpenRouter credit usage (in USD) for the current UTC month + */ + "usage_monthly": S.Number, + /** + * Total external BYOK usage (in USD) for the API key + */ + "byok_usage": S.Number, + /** + * External BYOK usage (in USD) for the current UTC day + */ + "byok_usage_daily": S.Number, + /** + * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + */ + "byok_usage_weekly": S.Number, + /** + * External BYOK usage (in USD) for current UTC month + */ + "byok_usage_monthly": S.Number, + /** + * ISO 8601 timestamp of when the API key was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the API key was last updated + */ + "updated_at": S.NullOr(S.String), + /** + * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + */ + "expires_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class ListGuardrailsParams extends S.Struct({ + /** + * Number of records to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListGuardrails200 extends S.Struct({ + /** + * List of guardrails + */ + "data": S.Array(S.Struct({ + /** + * Unique identifier for the guardrail + */ + "id": S.String, + /** + * Name of the guardrail + */ + "name": S.String, + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(S.Literal("daily", "weekly", "monthly"), { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Array of model canonical_slugs (immutable identifiers) + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * ISO 8601 timestamp of when the guardrail was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the guardrail was last updated + */ + "updated_at": S.optionalWith(S.String, { nullable: true }) + })), + /** + * Total number of guardrails + */ + "total_count": S.Number +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class CreateGuardrailRequestResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class CreateGuardrailRequest extends S.Class("CreateGuardrailRequest")({ + /** + * Name for the new guardrail + */ + "name": S.String.pipe(S.minLength(1), S.maxLength(200)), + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String.pipe(S.maxLength(1000)), { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(CreateGuardrailRequestResetInterval, { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + /** + * Array of model identifiers (slug or canonical_slug accepted) + */ + "allowed_models": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class CreateGuardrail201DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class CreateGuardrail201 extends S.Struct({ + /** + * The created guardrail + */ + "data": S.Struct({ + /** + * Unique identifier for the guardrail + */ + "id": S.String, + /** + * Name of the guardrail + */ + "name": S.String, + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(CreateGuardrail201DataResetInterval, { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Array of model canonical_slugs (immutable identifiers) + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * ISO 8601 timestamp of when the guardrail was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the guardrail was last updated + */ + "updated_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class GetGuardrail200DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class GetGuardrail200 extends S.Struct({ + /** + * The guardrail + */ + "data": S.Struct({ + /** + * Unique identifier for the guardrail + */ + "id": S.String, + /** + * Name of the guardrail + */ + "name": S.String, + /** + * Description of the guardrail + */ + "description": S.optionalWith(S.String, { nullable: true }), + /** + * Spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(GetGuardrail200DataResetInterval, { nullable: true }), + /** + * List of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Array of model canonical_slugs (immutable identifiers) + */ + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }), + /** + * ISO 8601 timestamp of when the guardrail was created + */ + "created_at": S.String, + /** + * ISO 8601 timestamp of when the guardrail was last updated + */ + "updated_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class DeleteGuardrail200 extends S.Struct({ + /** + * Confirmation that the guardrail was deleted + */ + "deleted": S.Literal(true) +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class UpdateGuardrailRequestResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class UpdateGuardrailRequest extends S.Class("UpdateGuardrailRequest")({ + /** + * New name for the guardrail + */ + "name": S.optionalWith(S.String.pipe(S.minLength(1), S.maxLength(200)), { nullable: true }), + /** + * New description for the guardrail + */ + "description": S.optionalWith(S.String.pipe(S.maxLength(1000)), { nullable: true }), + /** + * New spending limit in USD + */ + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), + /** + * Interval at which the limit resets (daily, weekly, monthly) + */ + "reset_interval": S.optionalWith(UpdateGuardrailRequestResetInterval, { nullable: true }), + /** + * New list of allowed provider IDs + */ + "allowed_providers": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + /** + * Array of model identifiers (slug or canonical_slug accepted) + */ + "allowed_models": S.optionalWith(S.NonEmptyArray(S.String).pipe(S.minItems(1)), { nullable: true }), + /** + * Whether to enforce zero data retention + */ + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }) +}) {} + +/** + * Interval at which the limit resets (daily, weekly, monthly) + */ +export class UpdateGuardrail200DataResetInterval extends S.Literal("daily", "weekly", "monthly") {} + +export class UpdateGuardrail200 extends S.Struct({ + /** + * The updated guardrail + */ + "data": S.Struct({ + /** + * Unique identifier for the guardrail + */ + "id": S.String, /** - * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + * Name of the guardrail */ - "usage_weekly": S.Number, + "name": S.String, /** - * OpenRouter credit usage (in USD) for the current UTC month + * Description of the guardrail */ - "usage_monthly": S.Number, + "description": S.optionalWith(S.String, { nullable: true }), /** - * Total external BYOK usage (in USD) for the API key + * Spending limit in USD */ - "byok_usage": S.Number, + "limit_usd": S.optionalWith(S.Number.pipe(S.greaterThan(0)), { nullable: true }), /** - * External BYOK usage (in USD) for the current UTC day + * Interval at which the limit resets (daily, weekly, monthly) */ - "byok_usage_daily": S.Number, + "reset_interval": S.optionalWith(UpdateGuardrail200DataResetInterval, { nullable: true }), /** - * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + * List of allowed provider IDs */ - "byok_usage_weekly": S.Number, + "allowed_providers": S.optionalWith(S.Array(S.String), { nullable: true }), /** - * External BYOK usage (in USD) for current UTC month + * Array of model canonical_slugs (immutable identifiers) */ - "byok_usage_monthly": S.Number, + "allowed_models": S.optionalWith(S.Array(S.String), { nullable: true }), /** - * ISO 8601 timestamp of when the API key was created + * Whether to enforce zero data retention */ - "created_at": S.String, + "enforce_zdr": S.optionalWith(S.Boolean, { nullable: true }), /** - * ISO 8601 timestamp of when the API key was last updated + * ISO 8601 timestamp of when the guardrail was created */ - "updated_at": S.NullOr(S.String), + "created_at": S.String, /** - * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + * ISO 8601 timestamp of when the guardrail was last updated */ - "expires_at": S.optionalWith(S.String, { nullable: true }) - }), + "updated_at": S.optionalWith(S.String, { nullable: true }) + }) +}) {} + +export class ListKeyAssignmentsParams extends S.Struct({ /** - * The actual API key string (only shown once) + * Number of records to skip for pagination */ - "key": S.String + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) }) {} -export class GetKey200 extends S.Struct({ +export class ListKeyAssignments200 extends S.Struct({ /** - * The API key information + * List of key assignments */ - "data": S.Struct({ - /** - * Unique hash identifier for the API key - */ - "hash": S.String, - /** - * Name of the API key - */ - "name": S.String, - /** - * Human-readable label for the API key - */ - "label": S.String, - /** - * Whether the API key is disabled - */ - "disabled": S.Boolean, - /** - * Spending limit for the API key in USD - */ - "limit": S.NullOr(S.Number), - /** - * Remaining spending limit in USD - */ - "limit_remaining": S.NullOr(S.Number), + "data": S.Array(S.Struct({ /** - * Type of limit reset for the API key + * Unique identifier for the assignment */ - "limit_reset": S.NullOr(S.String), + "id": S.String, /** - * Whether to include external BYOK usage in the credit limit + * Hash of the assigned API key */ - "include_byok_in_limit": S.Boolean, + "key_hash": S.String, /** - * Total OpenRouter credit usage (in USD) for the API key + * ID of the guardrail */ - "usage": S.Number, + "guardrail_id": S.String, /** - * OpenRouter credit usage (in USD) for the current UTC day + * Name of the API key */ - "usage_daily": S.Number, + "key_name": S.String, /** - * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + * Label of the API key */ - "usage_weekly": S.Number, + "key_label": S.String, /** - * OpenRouter credit usage (in USD) for the current UTC month + * User ID of who made the assignment */ - "usage_monthly": S.Number, + "assigned_by": S.NullOr(S.String), /** - * Total external BYOK usage (in USD) for the API key + * ISO 8601 timestamp of when the assignment was created */ - "byok_usage": S.Number, + "created_at": S.String + })), + /** + * Total number of key assignments for this guardrail + */ + "total_count": S.Number +}) {} + +export class ListMemberAssignmentsParams extends S.Struct({ + /** + * Number of records to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListMemberAssignments200 extends S.Struct({ + /** + * List of member assignments + */ + "data": S.Array(S.Struct({ /** - * External BYOK usage (in USD) for the current UTC day + * Unique identifier for the assignment */ - "byok_usage_daily": S.Number, + "id": S.String, /** - * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + * Clerk user ID of the assigned member */ - "byok_usage_weekly": S.Number, + "user_id": S.String, /** - * External BYOK usage (in USD) for current UTC month + * Organization ID */ - "byok_usage_monthly": S.Number, + "organization_id": S.String, /** - * ISO 8601 timestamp of when the API key was created + * ID of the guardrail */ - "created_at": S.String, + "guardrail_id": S.String, /** - * ISO 8601 timestamp of when the API key was last updated + * User ID of who made the assignment */ - "updated_at": S.NullOr(S.String), + "assigned_by": S.NullOr(S.String), /** - * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + * ISO 8601 timestamp of when the assignment was created */ - "expires_at": S.optionalWith(S.String, { nullable: true }) - }) -}) {} - -export class DeleteKeys200 extends S.Struct({ + "created_at": S.String + })), /** - * Confirmation that the API key was deleted + * Total number of member assignments */ - "deleted": S.Literal(true) + "total_count": S.Number }) {} -/** - * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. - */ -export class UpdateKeysRequestLimitReset extends S.Literal("daily", "weekly", "monthly") {} - -export class UpdateKeysRequest extends S.Class("UpdateKeysRequest")({ - /** - * New name for the API key - */ - "name": S.optionalWith(S.String, { nullable: true }), - /** - * Whether to disable the API key - */ - "disabled": S.optionalWith(S.Boolean, { nullable: true }), - /** - * New spending limit for the API key in USD - */ - "limit": S.optionalWith(S.Number, { nullable: true }), +export class ListGuardrailKeyAssignmentsParams extends S.Struct({ /** - * New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday. + * Number of records to skip for pagination */ - "limit_reset": S.optionalWith(UpdateKeysRequestLimitReset, { nullable: true }), + "offset": S.optionalWith(S.String, { nullable: true }), /** - * Whether to include BYOK usage in the limit + * Maximum number of records to return (max 100) */ - "include_byok_in_limit": S.optionalWith(S.Boolean, { nullable: true }) + "limit": S.optionalWith(S.String, { nullable: true }) }) {} -export class UpdateKeys200 extends S.Struct({ +export class ListGuardrailKeyAssignments200 extends S.Struct({ /** - * The updated API key information + * List of key assignments */ - "data": S.Struct({ - /** - * Unique hash identifier for the API key - */ - "hash": S.String, - /** - * Name of the API key - */ - "name": S.String, + "data": S.Array(S.Struct({ /** - * Human-readable label for the API key + * Unique identifier for the assignment */ - "label": S.String, + "id": S.String, /** - * Whether the API key is disabled + * Hash of the assigned API key */ - "disabled": S.Boolean, + "key_hash": S.String, /** - * Spending limit for the API key in USD + * ID of the guardrail */ - "limit": S.NullOr(S.Number), + "guardrail_id": S.String, /** - * Remaining spending limit in USD + * Name of the API key */ - "limit_remaining": S.NullOr(S.Number), + "key_name": S.String, /** - * Type of limit reset for the API key + * Label of the API key */ - "limit_reset": S.NullOr(S.String), + "key_label": S.String, /** - * Whether to include external BYOK usage in the credit limit + * User ID of who made the assignment */ - "include_byok_in_limit": S.Boolean, + "assigned_by": S.NullOr(S.String), /** - * Total OpenRouter credit usage (in USD) for the API key + * ISO 8601 timestamp of when the assignment was created */ - "usage": S.Number, + "created_at": S.String + })), + /** + * Total number of key assignments for this guardrail + */ + "total_count": S.Number +}) {} + +export class BulkAssignKeysToGuardrailRequest + extends S.Class("BulkAssignKeysToGuardrailRequest")({ /** - * OpenRouter credit usage (in USD) for the current UTC day + * Array of API key hashes to assign to the guardrail */ - "usage_daily": S.Number, + "key_hashes": S.NonEmptyArray(S.String.pipe(S.minLength(1))).pipe(S.minItems(1)) + }) +{} + +export class BulkAssignKeysToGuardrail200 extends S.Struct({ + /** + * Number of keys successfully assigned + */ + "assigned_count": S.Number +}) {} + +export class ListGuardrailMemberAssignmentsParams extends S.Struct({ + /** + * Number of records to skip for pagination + */ + "offset": S.optionalWith(S.String, { nullable: true }), + /** + * Maximum number of records to return (max 100) + */ + "limit": S.optionalWith(S.String, { nullable: true }) +}) {} + +export class ListGuardrailMemberAssignments200 extends S.Struct({ + /** + * List of member assignments + */ + "data": S.Array(S.Struct({ /** - * OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday) + * Unique identifier for the assignment */ - "usage_weekly": S.Number, + "id": S.String, /** - * OpenRouter credit usage (in USD) for the current UTC month + * Clerk user ID of the assigned member */ - "usage_monthly": S.Number, + "user_id": S.String, /** - * Total external BYOK usage (in USD) for the API key + * Organization ID */ - "byok_usage": S.Number, + "organization_id": S.String, /** - * External BYOK usage (in USD) for the current UTC day + * ID of the guardrail */ - "byok_usage_daily": S.Number, + "guardrail_id": S.String, /** - * External BYOK usage (in USD) for the current UTC week (Monday-Sunday) + * User ID of who made the assignment */ - "byok_usage_weekly": S.Number, + "assigned_by": S.NullOr(S.String), /** - * External BYOK usage (in USD) for current UTC month + * ISO 8601 timestamp of when the assignment was created */ - "byok_usage_monthly": S.Number, + "created_at": S.String + })), + /** + * Total number of member assignments + */ + "total_count": S.Number +}) {} + +export class BulkAssignMembersToGuardrailRequest + extends S.Class("BulkAssignMembersToGuardrailRequest")({ /** - * ISO 8601 timestamp of when the API key was created + * Array of member user IDs to assign to the guardrail */ - "created_at": S.String, + "member_user_ids": S.NonEmptyArray(S.String.pipe(S.minLength(1))).pipe(S.minItems(1)) + }) +{} + +export class BulkAssignMembersToGuardrail200 extends S.Struct({ + /** + * Number of members successfully assigned + */ + "assigned_count": S.Number +}) {} + +export class BulkUnassignKeysFromGuardrailRequest + extends S.Class("BulkUnassignKeysFromGuardrailRequest")({ /** - * ISO 8601 timestamp of when the API key was last updated + * Array of API key hashes to unassign from the guardrail */ - "updated_at": S.NullOr(S.String), + "key_hashes": S.NonEmptyArray(S.String.pipe(S.minLength(1))).pipe(S.minItems(1)) + }) +{} + +export class BulkUnassignKeysFromGuardrail200 extends S.Struct({ + /** + * Number of keys successfully unassigned + */ + "unassigned_count": S.Number +}) {} + +export class BulkUnassignMembersFromGuardrailRequest + extends S.Class("BulkUnassignMembersFromGuardrailRequest")({ /** - * ISO 8601 UTC timestamp when the API key expires, or null if no expiration + * Array of member user IDs to unassign from the guardrail */ - "expires_at": S.optionalWith(S.String, { nullable: true }) + "member_user_ids": S.NonEmptyArray(S.String.pipe(S.minLength(1))).pipe(S.minItems(1)) }) +{} + +export class BulkUnassignMembersFromGuardrail200 extends S.Struct({ + /** + * Number of members successfully unassigned + */ + "unassigned_count": S.Number }) {} export class GetCurrentKey200 extends S.Struct({ @@ -2905,12 +4568,12 @@ export class Schema0 extends S.Array( "Fireworks", "Friendli", "GMICloud", - "GoPomelo", "Google", "Google AI Studio", "Groq", "Hyperbolic", "Inception", + "Inceptron", "InferenceNet", "Infermatic", "Inflection", @@ -2935,13 +4598,14 @@ export class Schema0 extends S.Array( "Phala", "Relace", "SambaNova", + "Seed", "SiliconFlow", "Sourceful", "Stealth", "StreamLake", "Switchpoint", - "Targon", "Together", + "Upstage", "Venice", "WandB", "Xiaomi", @@ -3017,6 +4681,23 @@ export class ChatMessageToolCall extends S.Class("ChatMessa }) }) {} +export class Schema3 extends S.Union(S.String, S.Null) {} + +export class Schema4Enum extends S.Literal( + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" +) {} + +export class Schema4 extends S.Union(Schema4Enum, S.Null) {} + +export class Schema5 extends S.Number {} + +export class Schema2 extends S.Record({ key: S.String, value: S.Unknown }) {} + export class AssistantMessage extends S.Class("AssistantMessage")({ "role": S.Literal("assistant"), "content": S.optionalWith(S.Union(S.String, S.Array(ChatMessageContentItem)), { nullable: true }), @@ -3025,7 +4706,14 @@ export class AssistantMessage extends S.Class("AssistantMessag "refusal": S.optionalWith(S.String, { nullable: true }), "reasoning": S.optionalWith(S.String, { nullable: true }), "reasoning_details": S.optionalWith(S.Array(ReasoningDetail), { nullable: true }), - "images": S.optionalWith(S.Array(ChatMessageContentItemImage), { nullable: true }), + "images": S.optionalWith( + S.Array(S.Struct({ + "image_url": S.Struct({ + "url": S.String + }) + })), + { nullable: true } + ), "annotations": S.optionalWith(S.Array(AnnotationDetail), { nullable: true }) }) {} @@ -3146,10 +4834,36 @@ export class ChatGenerationParams extends S.Class("ChatGen }), { nullable: true } ), - "preferred_min_throughput": S.optionalWith(S.Number, { nullable: true }), - "preferred_max_latency": S.optionalWith(S.Number, { nullable: true }), - "min_throughput": S.optionalWith(S.Number, { nullable: true }), - "max_latency": S.optionalWith(S.Number, { nullable: true }) + /** + * Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. + */ + "preferred_min_throughput": S.optionalWith( + S.Union( + S.Number, + S.Struct({ + "p50": S.optionalWith(S.Number, { nullable: true }), + "p75": S.optionalWith(S.Number, { nullable: true }), + "p90": S.optionalWith(S.Number, { nullable: true }), + "p99": S.optionalWith(S.Number, { nullable: true }) + }) + ), + { nullable: true } + ), + /** + * Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold. + */ + "preferred_max_latency": S.optionalWith( + S.Union( + S.Number, + S.Struct({ + "p50": S.optionalWith(S.Number, { nullable: true }), + "p75": S.optionalWith(S.Number, { nullable: true }), + "p90": S.optionalWith(S.Number, { nullable: true }), + "p99": S.optionalWith(S.Number, { nullable: true }) + }) + ), + { nullable: true } + ) }), { nullable: true } ), @@ -3207,24 +4921,14 @@ export class ChatGenerationParams extends S.Class("ChatGen "echo_upstream_body": S.optionalWith(S.Boolean, { nullable: true }) }), { nullable: true } - ) + ), + "image_config": S.optionalWith(S.Record({ key: S.String, value: S.Unknown }), { nullable: true }), + "modalities": S.optionalWith(S.Array(S.Literal("text", "image")), { nullable: true }) }) {} export class ChatCompletionFinishReason extends S.Literal("tool_calls", "stop", "length", "content_filter", "error") {} -export class Schema2 extends S.Union(ChatCompletionFinishReason, S.Null) {} - -export class Schema4 extends S.Union(S.String, S.Null) {} - -export class Schema5Enum - extends S.Literal("unknown", "openai-responses-v1", "xai-responses-v1", "anthropic-claude-v1", "google-gemini-v1") -{} - -export class Schema5 extends S.Union(Schema5Enum, S.Null) {} - -export class Schema6 extends S.Number {} - -export class Schema3 extends S.Record({ key: S.String, value: S.Unknown }) {} +export class Schema6 extends S.Union(ChatCompletionFinishReason, S.Null) {} export class ChatMessageTokenLogprob extends S.Class("ChatMessageTokenLogprob")({ "token": S.String, @@ -3246,7 +4950,6 @@ export class ChatResponseChoice extends S.Class("ChatRespons "finish_reason": S.NullOr(ChatCompletionFinishReason), "index": S.Number, "message": AssistantMessage, - "reasoning_details": S.optionalWith(S.Array(Schema3), { nullable: true }), "logprobs": S.optionalWith(ChatMessageTokenLogprobs, { nullable: true }) }) {} @@ -3254,13 +4957,6 @@ export class ChatGenerationTokenUsage extends S.Class( "completion_tokens": S.Number, "prompt_tokens": S.Number, "total_tokens": S.Number, - "cost": S.optionalWith(S.Number, { nullable: true }), - "cost_details": S.optionalWith( - S.Struct({ - upstream_inference_cost: S.optionalWith(S.Number, { nullable: true }) - }), - { nullable: true } - ), "completion_tokens_details": S.optionalWith( S.Struct({ "reasoning_tokens": S.optionalWith(S.Number, { nullable: true }), @@ -3273,11 +4969,16 @@ export class ChatGenerationTokenUsage extends S.Class( "prompt_tokens_details": S.optionalWith( S.Struct({ "cached_tokens": S.optionalWith(S.Number, { nullable: true }), + "cache_write_tokens": S.optionalWith(S.Number, { nullable: true }), "audio_tokens": S.optionalWith(S.Number, { nullable: true }), "video_tokens": S.optionalWith(S.Number, { nullable: true }) }), { nullable: true } - ) + ), + "cost": S.optionalWith(S.Number, { nullable: true }), + "cost_details": S.optionalWith(S.Struct({ upstream_inference_cost: S.optionalWith(S.Number, { nullable: true }) }), { + nullable: true + }) }) {} export class ChatResponse extends S.Class("ChatResponse")({ @@ -3436,6 +5137,22 @@ export const make = ( orElse: unexpectedStatus })) ), + "createMessages": (options) => + HttpClientRequest.post(`/messages`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AnthropicMessagesResponse), + "400": decodeError("CreateMessages400", CreateMessages400), + "401": decodeError("CreateMessages401", CreateMessages401), + "403": decodeError("CreateMessages403", CreateMessages403), + "404": decodeError("CreateMessages404", CreateMessages404), + "429": decodeError("CreateMessages429", CreateMessages429), + "500": decodeError("CreateMessages500", CreateMessages500), + "503": decodeError("CreateMessages503", CreateMessages503), + "529": decodeError("CreateMessages529", CreateMessages529), + orElse: unexpectedStatus + })) + ), "getUserActivity": (options) => HttpClientRequest.get(`/activity`).pipe( HttpClientRequest.setUrlParams({ "date": options?.["date"] as any }), @@ -3560,17 +5277,6 @@ export const make = ( orElse: unexpectedStatus })) ), - "getParameters": (author, slug, options) => - HttpClientRequest.get(`/parameters/${author}/${slug}`).pipe( - HttpClientRequest.setUrlParams({ "provider": options?.["provider"] as any }), - withResponse(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetParameters200), - "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), - "404": decodeError("NotFoundResponse", NotFoundResponse), - "500": decodeError("InternalServerResponse", InternalServerResponse), - orElse: unexpectedStatus - })) - ), "listProviders": () => HttpClientRequest.get(`/providers`).pipe( withResponse(HttpClientResponse.matchStatus({ @@ -3640,6 +5346,149 @@ export const make = ( orElse: unexpectedStatus })) ), + "listGuardrails": (options) => + HttpClientRequest.get(`/guardrails`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrails200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "createGuardrail": (options) => + HttpClientRequest.post(`/guardrails`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateGuardrail201), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "getGuardrail": (id) => + HttpClientRequest.get(`/guardrails/${id}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetGuardrail200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "deleteGuardrail": (id) => + HttpClientRequest.del(`/guardrails/${id}`).pipe( + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteGuardrail200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "updateGuardrail": (id, options) => + HttpClientRequest.patch(`/guardrails/${id}`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listKeyAssignments": (options) => + HttpClientRequest.get(`/guardrails/assignments/keys`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListKeyAssignments200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listMemberAssignments": (options) => + HttpClientRequest.get(`/guardrails/assignments/members`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListMemberAssignments200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listGuardrailKeyAssignments": (id, options) => + HttpClientRequest.get(`/guardrails/${id}/assignments/keys`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrailKeyAssignments200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "bulkAssignKeysToGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/keys`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAssignKeysToGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "listGuardrailMemberAssignments": (id, options) => + HttpClientRequest.get(`/guardrails/${id}/assignments/members`).pipe( + HttpClientRequest.setUrlParams({ "offset": options?.["offset"] as any, "limit": options?.["limit"] as any }), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrailMemberAssignments200), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "bulkAssignMembersToGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/members`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAssignMembersToGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "bulkUnassignKeysFromGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/keys/remove`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkUnassignKeysFromGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), + "bulkUnassignMembersFromGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/members/remove`).pipe( + HttpClientRequest.bodyUnsafeJson(options), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkUnassignMembersFromGuardrail200), + "400": decodeError("BadRequestResponse", BadRequestResponse), + "401": decodeError("UnauthorizedResponse", UnauthorizedResponse), + "404": decodeError("NotFoundResponse", NotFoundResponse), + "500": decodeError("InternalServerResponse", InternalServerResponse), + orElse: unexpectedStatus + })) + ), "getCurrentKey": () => HttpClientRequest.get(`/key`).pipe( withResponse(HttpClientResponse.matchStatus({ @@ -3724,7 +5573,25 @@ export interface Client { | ClientError<"ProviderOverloadedResponse", typeof ProviderOverloadedResponse.Type> > /** - * Returns user activity data grouped by endpoint for the last 30 (completed) UTC days + * Creates a message using the Anthropic Messages API format. Supports text, images, PDFs, tools, and extended thinking. + */ + readonly "createMessages": ( + options: typeof AnthropicMessagesRequest.Encoded + ) => Effect.Effect< + typeof AnthropicMessagesResponse.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"CreateMessages400", typeof CreateMessages400.Type> + | ClientError<"CreateMessages401", typeof CreateMessages401.Type> + | ClientError<"CreateMessages403", typeof CreateMessages403.Type> + | ClientError<"CreateMessages404", typeof CreateMessages404.Type> + | ClientError<"CreateMessages429", typeof CreateMessages429.Type> + | ClientError<"CreateMessages500", typeof CreateMessages500.Type> + | ClientError<"CreateMessages503", typeof CreateMessages503.Type> + | ClientError<"CreateMessages529", typeof CreateMessages529.Type> + > + /** + * Returns user activity data grouped by endpoint for the last 30 (completed) UTC days. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. */ readonly "getUserActivity": ( options?: typeof GetUserActivityParams.Encoded | undefined @@ -3738,7 +5605,7 @@ export interface Client { | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** - * Get total credits purchased and used for the authenticated user + * Get total credits purchased and used for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. */ readonly "getCredits": () => Effect.Effect< typeof GetCredits200.Type, @@ -3863,21 +5730,6 @@ export interface Client { | ParseError | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > - /** - * Get a model's supported parameters and data about which are most popular - */ - readonly "getParameters": ( - author: string, - slug: string, - options?: typeof GetParametersParams.Encoded | undefined - ) => Effect.Effect< - typeof GetParameters200.Type, - | HttpClientError.HttpClientError - | ParseError - | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> - | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> - | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> - > /** * List all providers */ @@ -3888,7 +5740,7 @@ export interface Client { | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** - * List API keys + * List all API keys for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. */ readonly "list": ( options?: typeof ListParams.Encoded | undefined @@ -3901,7 +5753,7 @@ export interface Client { | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** - * Create a new API key + * Create a new API key for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. */ readonly "createKeys": ( options: typeof CreateKeysRequest.Encoded @@ -3915,7 +5767,7 @@ export interface Client { | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** - * Get a single API key + * Get a single API key by hash. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. */ readonly "getKey": ( hash: string @@ -3929,7 +5781,7 @@ export interface Client { | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** - * Delete an API key + * Delete an existing API key. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. */ readonly "deleteKeys": ( hash: string @@ -3943,7 +5795,7 @@ export interface Client { | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > /** - * Update an API key + * Update an existing API key. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. */ readonly "updateKeys": ( hash: string, @@ -3958,6 +5810,184 @@ export interface Client { | ClientError<"TooManyRequestsResponse", typeof TooManyRequestsResponse.Type> | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> > + /** + * List all guardrails for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listGuardrails": ( + options?: typeof ListGuardrailsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListGuardrails200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Create a new guardrail for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "createGuardrail": ( + options: typeof CreateGuardrailRequest.Encoded + ) => Effect.Effect< + typeof CreateGuardrail201.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Get a single guardrail by ID. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "getGuardrail": ( + id: string + ) => Effect.Effect< + typeof GetGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Delete an existing guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "deleteGuardrail": ( + id: string + ) => Effect.Effect< + typeof DeleteGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Update an existing guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "updateGuardrail": ( + id: string, + options: typeof UpdateGuardrailRequest.Encoded + ) => Effect.Effect< + typeof UpdateGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all API key guardrail assignments for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listKeyAssignments": ( + options?: typeof ListKeyAssignmentsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListKeyAssignments200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all organization member guardrail assignments for the authenticated user. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listMemberAssignments": ( + options?: typeof ListMemberAssignmentsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListMemberAssignments200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all API key assignments for a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listGuardrailKeyAssignments": ( + id: string, + options?: typeof ListGuardrailKeyAssignmentsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListGuardrailKeyAssignments200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Assign multiple API keys to a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "bulkAssignKeysToGuardrail": ( + id: string, + options: typeof BulkAssignKeysToGuardrailRequest.Encoded + ) => Effect.Effect< + typeof BulkAssignKeysToGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * List all organization member assignments for a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "listGuardrailMemberAssignments": ( + id: string, + options?: typeof ListGuardrailMemberAssignmentsParams.Encoded | undefined + ) => Effect.Effect< + typeof ListGuardrailMemberAssignments200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Assign multiple organization members to a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "bulkAssignMembersToGuardrail": ( + id: string, + options: typeof BulkAssignMembersToGuardrailRequest.Encoded + ) => Effect.Effect< + typeof BulkAssignMembersToGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Unassign multiple API keys from a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "bulkUnassignKeysFromGuardrail": ( + id: string, + options: typeof BulkUnassignKeysFromGuardrailRequest.Encoded + ) => Effect.Effect< + typeof BulkUnassignKeysFromGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > + /** + * Unassign multiple organization members from a specific guardrail. [Provisioning key](/docs/guides/overview/auth/provisioning-api-keys) required. + */ + readonly "bulkUnassignMembersFromGuardrail": ( + id: string, + options: typeof BulkUnassignMembersFromGuardrailRequest.Encoded + ) => Effect.Effect< + typeof BulkUnassignMembersFromGuardrail200.Type, + | HttpClientError.HttpClientError + | ParseError + | ClientError<"BadRequestResponse", typeof BadRequestResponse.Type> + | ClientError<"UnauthorizedResponse", typeof UnauthorizedResponse.Type> + | ClientError<"NotFoundResponse", typeof NotFoundResponse.Type> + | ClientError<"InternalServerResponse", typeof InternalServerResponse.Type> + > /** * Get information on the API key associated with the current authentication session */ diff --git a/packages/ai/openrouter/src/OpenRouterClient.ts b/packages/ai/openrouter/src/OpenRouterClient.ts index ec61801571f..9cd54a6c56d 100644 --- a/packages/ai/openrouter/src/OpenRouterClient.ts +++ b/packages/ai/openrouter/src/OpenRouterClient.ts @@ -102,7 +102,7 @@ export const make: (options: { request.pipe( HttpClientRequest.prependUrl(options.apiUrl ?? "https://openrouter.ai/api/v1"), options.apiKey ? HttpClientRequest.bearerToken(options.apiKey) : identity, - options.referrer ? HttpClientRequest.setHeader("HTTP-Referrer", options.referrer) : identity, + options.referrer ? HttpClientRequest.setHeader("HTTP-Referer", options.referrer) : identity, options.title ? HttpClientRequest.setHeader("X-Title", options.title) : identity, HttpClientRequest.acceptJson ) @@ -313,10 +313,10 @@ export class ChatStreamingMessageToolCall extends Schema.Class 0) { - emitReasoningPart(delta.reasoning) - } - - if (Predicate.isNotNullable(delta.reasoning_details) && delta.reasoning_details.length > 0) { + if (Predicate.isNotNullable(delta?.reasoning_details) && delta.reasoning_details.length > 0) { for (const detail of delta.reasoning_details) { switch (detail.type) { case "reasoning.summary": { @@ -830,11 +822,13 @@ const makeStreamResponse: (stream: Stream.Stream 0) { + emitReasoningPart(delta.reasoning) } // Text Parts - if (Predicate.isNotNullable(delta.content) && delta.content.length > 0) { + if (Predicate.isNotNullable(delta?.content) && delta.content.length > 0) { // End in-progress reasoning part if present before starting text if (Predicate.isNotUndefined(activeReasoningId)) { parts.push({ @@ -861,7 +855,7 @@ const makeStreamResponse: (stream: Stream.Stream 0) { + if (Predicate.isNotNullable(delta?.tool_calls) && delta.tool_calls.length > 0) { for (const toolCall of delta.tool_calls) { // Get the active tool call, if present let activeToolCall = activeToolCalls[toolCall.index] @@ -948,7 +942,7 @@ const makeStreamResponse: (stream: Stream.Stream( const baseArgs = Arr.take(originalArgs, 2) const filteredArgs: Array = [] for (let i = 0; i < args.length; i++) { - if (isLogLevelArg(args[i]) || isLogLevelArg(args[i - 1])) { + if (isLogLevelArg(args[i]) || args[i - 1] === "--log-level") { continue } filteredArgs.push(args[i]) diff --git a/packages/cli/src/internal/prompt/text.ts b/packages/cli/src/internal/prompt/text.ts index 28046770613..1cfc3560ef7 100644 --- a/packages/cli/src/internal/prompt/text.ts +++ b/packages/cli/src/internal/prompt/text.ts @@ -49,7 +49,10 @@ function renderClearScreen(state: State, options: Options) { ) }) // Ensure that the prior prompt output is cleaned up - const clearOutput = InternalAnsiUtils.eraseText(options.message, columns) + // Calculate full rendered line: "? " + message + " › " + input + const inputValue = state.value.length > 0 ? state.value : options.default + const fullLine = `? ${options.message} \u203a ${inputValue}` + const clearOutput = InternalAnsiUtils.eraseText(fullLine, columns) // Concatenate and render all documents return clearError.pipe( Doc.cat(clearOutput), diff --git a/packages/cli/test/CliApp.test.ts b/packages/cli/test/CliApp.test.ts index de5dc108ee6..432d0ea7b85 100644 --- a/packages/cli/test/CliApp.test.ts +++ b/packages/cli/test/CliApp.test.ts @@ -130,5 +130,38 @@ describe("CliApp", () => { yield* cli(["C:\\Program Files\\node.exe", "C:\\My Scripts\\test.js", "--log-level", "info", "hello"]) expect(executedValue).toEqual("hello") }).pipe(runEffect)) + + it("should not swallow the next argument when using --log-level=value equals syntax", () => + Effect.gen(function*() { + let executedValue: string | undefined = undefined + const cmd = Command.make("test", { value: Args.text() }, ({ value }) => + Effect.sync(() => { + executedValue = value + })) + const cli = Command.run(cmd, { + name: "Test", + version: "1.0.0" + }) + yield* cli(["node", "test.js", "--log-level=debug", "hello"]) + expect(executedValue).toEqual("hello") + }).pipe(runEffect)) + + it("should set log level and preserve argument with --log-level=value combined", () => + Effect.gen(function*() { + let logLevel: LogLevel.LogLevel | undefined = undefined + let executedValue: string | undefined = undefined + const cmd = Command.make("test", { value: Args.text() }, ({ value }) => + Effect.gen(function*() { + logLevel = yield* FiberRef.get(FiberRef.currentMinimumLogLevel) + executedValue = value + })) + const cli = Command.run(cmd, { + name: "Test", + version: "1.0.0" + }) + yield* cli(["node", "test.js", "--log-level=info", "hello"]) + expect(logLevel).toEqual(LogLevel.Info) + expect(executedValue).toEqual("hello") + }).pipe(runEffect)) }) }) diff --git a/packages/cluster/CHANGELOG.md b/packages/cluster/CHANGELOG.md index 3f56938fcfc..9b6f5314dcf 100644 --- a/packages/cluster/CHANGELOG.md +++ b/packages/cluster/CHANGELOG.md @@ -1,5 +1,74 @@ # @effect/cluster +## 0.58.2 + +### Patch Changes + +- [#6110](https://github.com/Effect-TS/effect/pull/6110) [`0fac630`](https://github.com/Effect-TS/effect/commit/0fac630b27095ffbfa6c48851087950ddc29cda0) Thanks @mitre88! - fix: correct typos in source code (receive, separate) + +- Updated dependencies [[`74f3267`](https://github.com/Effect-TS/effect/commit/74f3267a6cc7ed7818c4c34cc1232f7cfc7d3339), [`518d0e3`](https://github.com/Effect-TS/effect/commit/518d0e3f4879be6d9d9a7fa137a1820604bb3ea7), [`c016642`](https://github.com/Effect-TS/effect/commit/c0166426f80b7eb8e7f7d3aecc95dcd4fdb5cb55), [`0fac630`](https://github.com/Effect-TS/effect/commit/0fac630b27095ffbfa6c48851087950ddc29cda0), [`e2374c2`](https://github.com/Effect-TS/effect/commit/e2374c20ce699d9f5340baf744cf1bd67bb220a0)]: + - effect@3.21.2 + - @effect/platform@0.96.1 + - @effect/rpc@0.75.1 + - @effect/sql@0.51.1 + +## 0.58.1 + +### Patch Changes + +- [#6183](https://github.com/Effect-TS/effect/pull/6183) [`4708bb8`](https://github.com/Effect-TS/effect/commit/4708bb8e327f24651ab9072221289d8214c4e2df) Thanks @tim-smart! - backport cluster serialization fix for notify path + +- Updated dependencies [[`f99048e`](https://github.com/Effect-TS/effect/commit/f99048e9f4b89ce1afe31e1827dee5d751ddaa5b)]: + - effect@3.21.1 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + - @effect/rpc@0.75.0 + - @effect/sql@0.51.0 + - @effect/workflow@0.18.0 + +## 0.57.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + - @effect/rpc@0.74.0 + - @effect/sql@0.50.0 + - @effect/workflow@0.17.0 + +## 0.56.4 + +### Patch Changes + +- [#6056](https://github.com/Effect-TS/effect/pull/6056) [`52cd531`](https://github.com/Effect-TS/effect/commit/52cd53115fe14e2b9c36dd0d51eb490aead74ee6) Thanks @0xh3x! - use HttpLayerRouter variants for route registration in HttpRunner + +- Updated dependencies [[`d67c708`](https://github.com/Effect-TS/effect/commit/d67c7089ba8616b2d48ef7324312267a2a6f310a), [`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f), [`598ff76`](https://github.com/Effect-TS/effect/commit/598ff7642fdee7f3379bca49e378a0e9647bbe75)]: + - @effect/platform@0.94.5 + - effect@3.19.17 + - @effect/rpc@0.73.1 + +## 0.56.3 + +### Patch Changes + +- [#6033](https://github.com/Effect-TS/effect/pull/6033) [`740a912`](https://github.com/Effect-TS/effect/commit/740a912142c2578defcf3e1e7d449535b074bd61) Thanks @tim-smart! - Add `PgClient.fromPool` and `PgClient.layerFromPool`. + +- Updated dependencies [[`22d9d27`](https://github.com/Effect-TS/effect/commit/22d9d27bc007db86d9e4748c17324fab5f950c7d)]: + - @effect/platform@0.94.4 + +## 0.56.2 + +### Patch Changes + +- [#6031](https://github.com/Effect-TS/effect/pull/6031) [`1781244`](https://github.com/Effect-TS/effect/commit/17812444c6c0d8f19f9fbc85d82f911dff5523ab) Thanks @tim-smart! - backport effect v4 MessageStorage improvements + ## 0.56.1 ### Patch Changes diff --git a/packages/cluster/package.json b/packages/cluster/package.json index 701c88aef19..1f8ef8badf3 100644 --- a/packages/cluster/package.json +++ b/packages/cluster/package.json @@ -1,7 +1,7 @@ { "name": "@effect/cluster", "type": "module", - "version": "0.56.1", + "version": "0.58.2", "description": "Unified interfaces for common cluster-specific services", "publishConfig": { "access": "public", @@ -50,7 +50,9 @@ "@effect/workflow": "workspace:^", "@testcontainers/mysql": "^10.25.0", "@testcontainers/postgresql": "^10.25.0", - "effect": "workspace:^" + "@types/pg": "^8.15.6", + "effect": "workspace:^", + "pg": "^8.16.3" }, "dependencies": { "kubernetes-types": "^1.30.0" diff --git a/packages/cluster/src/HttpRunner.ts b/packages/cluster/src/HttpRunner.ts index 2985882a4e3..08130dfb792 100644 --- a/packages/cluster/src/HttpRunner.ts +++ b/packages/cluster/src/HttpRunner.ts @@ -172,7 +172,7 @@ export const layerHttpOptions = (options: { | HttpRouter.HttpRouter > => RunnerServer.layerWithClients.pipe( - Layer.provide(RpcServer.layerProtocolHttp(options)) + Layer.provide(RpcServer.layerProtocolHttpRouter(options)) ) /** @@ -193,7 +193,7 @@ export const layerWebsocketOptions = (options: { | HttpRouter.HttpRouter > => RunnerServer.layerWithClients.pipe( - Layer.provide(RpcServer.layerProtocolWebsocket(options)) + Layer.provide(RpcServer.layerProtocolWebsocketRouter(options)) ) /** diff --git a/packages/cluster/src/MessageStorage.ts b/packages/cluster/src/MessageStorage.ts index 37b052becab..492c3ac1287 100644 --- a/packages/cluster/src/MessageStorage.ts +++ b/packages/cluster/src/MessageStorage.ts @@ -251,7 +251,7 @@ export type Encoded = { * - Un-acknowledged chunk replies * - WithExit replies */ - readonly repliesFor: (requestIds: Array) => Effect.Effect< + readonly repliesFor: (requestIds: Arr.NonEmptyArray) => Effect.Effect< Array>, PersistenceError > @@ -259,7 +259,7 @@ export type Encoded = { /** * Retrieves the replies for the specified request ids. */ - readonly repliesForUnfiltered: (requestIds: Array) => Effect.Effect< + readonly repliesForUnfiltered: (requestIds: Arr.NonEmptyArray) => Effect.Effect< Array>, PersistenceError > @@ -275,7 +275,7 @@ export type Encoded = { * - All Interrupt's for unprocessed requests */ readonly unprocessedMessages: ( - shardIds: ReadonlyArray, + shardIds: Arr.NonEmptyArray, now: number ) => Effect.Effect< Array<{ @@ -289,7 +289,7 @@ export type Encoded = { * Retrieves the unprocessed messages by id. */ readonly unprocessedMessagesById: ( - messageIds: ReadonlyArray, + messageIds: Arr.NonEmptyArray, now: number ) => Effect.Effect< Array<{ @@ -317,7 +317,7 @@ export type Encoded = { * Reset the mailbox state for the provided shards. */ readonly resetShards: ( - shardIds: ReadonlyArray + shardIds: Arr.NonEmptyArray ) => Effect.Effect } @@ -504,28 +504,31 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect< requestIds.push(id) map.set(id, message) } - if (requestIds.length === 0) return [] + if (!Arr.isNonEmptyArray(requestIds)) return [] const encodedReplies = yield* encoded.repliesFor(requestIds) return yield* decodeReplies(map, encodedReplies) }), - repliesForUnfiltered: (ids) => encoded.repliesForUnfiltered(Array.from(ids, String)), + repliesForUnfiltered: (ids) => { + const requestIds = Array.from(ids, String) + return Arr.isNonEmptyArray(requestIds) + ? encoded.repliesForUnfiltered(requestIds) + : Effect.succeed([]) + }, requestIdForPrimaryKey(options) { const primaryKey = Envelope.primaryKeyByAddress(options) return encoded.requestIdForPrimaryKey(primaryKey) }, unprocessedMessages: (shardIds) => { - const shards = Array.from(shardIds) - if (shards.length === 0) return Effect.succeed([]) + const shards = Array.from(shardIds, (id) => id.toString()) + if (!Arr.isNonEmptyArray(shards)) return Effect.succeed([]) return Effect.flatMap( - Effect.suspend(() => - encoded.unprocessedMessages(shards.map((id) => id.toString()), clock.unsafeCurrentTimeMillis()) - ), + Effect.suspend(() => encoded.unprocessedMessages(shards, clock.unsafeCurrentTimeMillis())), decodeMessages ) }, unprocessedMessagesById(messageIds) { const ids = Array.from(messageIds) - if (ids.length === 0) return Effect.succeed([]) + if (!Arr.isNonEmptyArray(ids)) return Effect.succeed([]) return Effect.flatMap( Effect.suspend(() => encoded.unprocessedMessagesById(ids, clock.unsafeCurrentTimeMillis())), decodeMessages @@ -533,7 +536,12 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect< }, resetAddress: encoded.resetAddress, clearAddress: encoded.clearAddress, - resetShards: (shardIds) => encoded.resetShards(Array.from(shardIds, (id) => id.toString())) + resetShards: (shardIds) => { + const shards = Array.from(shardIds, (id) => id.toString()) + return Arr.isNonEmptyArray(shards) + ? encoded.resetShards(shards) + : Effect.void + } }) const decodeMessages = ( diff --git a/packages/cluster/src/RunnerServer.ts b/packages/cluster/src/RunnerServer.ts index b09e960794f..3d651851b04 100644 --- a/packages/cluster/src/RunnerServer.ts +++ b/packages/cluster/src/RunnerServer.ts @@ -139,7 +139,7 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() { const constWaitUntilRead = { waitUntilRead: true } as const /** - * The `RunnerServer` recieves messages from other Runners and forwards them to the + * The `RunnerServer` receives messages from other Runners and forwards them to the * `Sharding` layer. * * It also responds to `Ping` requests. @@ -179,7 +179,7 @@ export const layerWithClients: Layer.Layer< /** * A `Runners` layer that is client only. * - * It will not register with RunnerStorage and recieve shard assignments, + * It will not register with RunnerStorage and receive shard assignments, * so this layer can be used to embed a cluster client inside another effect * application. * diff --git a/packages/cluster/src/Runners.ts b/packages/cluster/src/Runners.ts index e675f24362c..c69b9a7764c 100644 --- a/packages/cluster/src/Runners.ts +++ b/packages/cluster/src/Runners.ts @@ -590,12 +590,16 @@ export const makeRpc: Effect.Effect< if (Option.isNone(address)) { return Effect.void } - const envelope = message.envelope - return RcMap.get(clients, address.value).pipe( - Effect.flatMap((client) => client.Notify({ envelope })), - Effect.scoped, - Effect.ignore - ) + const encode: Effect.Effect = + message._tag === "OutgoingRequest" + ? Effect.orDie(Message.serializeRequest(message)) + : Effect.succeed(message.envelope) + return Effect.flatMap(encode, (envelope) => + RcMap.get(clients, address.value).pipe( + Effect.flatMap((client) => client.Notify({ envelope })), + Effect.scoped, + Effect.ignore + )) }, onRunnerUnavailable: (address) => RcMap.invalidate(clients, address) }) diff --git a/packages/cluster/src/SqlMessageStorage.ts b/packages/cluster/src/SqlMessageStorage.ts index 28465e08814..4326c3832b4 100644 --- a/packages/cluster/src/SqlMessageStorage.ts +++ b/packages/cluster/src/SqlMessageStorage.ts @@ -308,7 +308,7 @@ export const make = Effect.fnUntraced(function*(options?: { }) const getUnprocessedMessages = sql.onDialectOrElse({ - pg: () => (shardIds: ReadonlyArray, now: number) => + pg: () => (shardIds: Arr.NonEmptyArray, now: number) => sql` WITH messages AS ( UPDATE ${messagesTableSql} m @@ -333,7 +333,7 @@ export const make = Effect.fnUntraced(function*(options?: { ) SELECT * FROM messages ORDER BY rowid ASC `, - orElse: () => (shardIds: ReadonlyArray, now: number) => + orElse: () => (shardIds: Arr.NonEmptyArray, now: number) => sql` SELECT m.*, r.id as reply_reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence FROM ${messagesTableSql} m @@ -514,7 +514,7 @@ export const make = Effect.fnUntraced(function*(options?: { ), unprocessedMessagesById(ids, now) { - const idArr = Array.from(ids, (id) => String(id)) + const idArr = ids.map((id) => String(id)) return sql` SELECT m.*, r.id as reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence FROM ${messagesTableSql} m diff --git a/packages/cluster/test/HttpRunner.test.ts b/packages/cluster/test/HttpRunner.test.ts index ec8eb207fab..4ee282be549 100644 --- a/packages/cluster/test/HttpRunner.test.ts +++ b/packages/cluster/test/HttpRunner.test.ts @@ -1,10 +1,11 @@ import * as HttpClient from "@effect/platform/HttpClient" import * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" import { RpcSerialization } from "@effect/rpc" import { describe, expect, it } from "@effect/vitest" import { Effect, Layer, Ref } from "effect" import * as HttpRunner from "../src/HttpRunner.js" -import { RunnerAddress, Runners } from "../src/index.js" +import { MessageStorage, RunnerAddress, RunnerHealth, Runners, RunnerStorage, ShardingConfig } from "../src/index.js" describe("HttpRunner", () => { describe("layerClientProtocolHttp", () => { @@ -103,4 +104,34 @@ describe("HttpRunner", () => { expect(urls[0]).toBe("http://localhost:3000/rpc") })) }) + + describe("layerHttpOptions", () => { + const deps = Layer.mergeAll( + RunnerStorage.layerMemory, + RunnerHealth.layerNoop, + MessageStorage.layerNoop, + ShardingConfig.layerDefaults, + RpcSerialization.layerNdjson, + Layer.succeed(Runners.RpcClientProtocol, () => Effect.die("mock")) + ) + + it.scoped("registers route on HttpLayerRouter so POST / is not 404", () => + Effect.gen(function*() { + const { dispose, handler } = HttpLayerRouter.toWebHandler( + HttpRunner.layerHttpOptions({ path: "/" }).pipe(Layer.provide(deps)) + ) + yield* Effect.addFinalizer(() => Effect.promise(() => dispose())) + + const response = yield* Effect.promise(() => + handler( + new Request("http://localhost/", { + method: "POST", + headers: { "content-type": "application/octet-stream" }, + body: new Uint8Array([]) + }) + ) + ) + expect(response.status).not.toBe(404) + })) + }) }) diff --git a/packages/cluster/test/MessageStorage.test.ts b/packages/cluster/test/MessageStorage.test.ts index f58130f7a31..20703937f39 100644 --- a/packages/cluster/test/MessageStorage.test.ts +++ b/packages/cluster/test/MessageStorage.test.ts @@ -108,6 +108,34 @@ describe("MessageStorage", () => { yield* fiber.await }).pipe(Effect.provide(MemoryLive))) }) + + describe("makeEncoded", () => { + it.effect("guards empty id lists before delegating", () => + Effect.gen(function*() { + const encoded = { + saveEnvelope: () => Effect.succeed(MessageStorage.SaveResultEncoded.Success()), + saveReply: () => Effect.void, + clearReplies: () => Effect.void, + requestIdForPrimaryKey: () => Effect.succeed(Option.none()), + repliesFor: () => Effect.succeed([]), + repliesForUnfiltered: () => Effect.die("unexpected repliesForUnfiltered call"), + unprocessedMessages: () => Effect.succeed([]), + unprocessedMessagesById: () => Effect.succeed([]), + resetAddress: () => Effect.void, + clearAddress: () => Effect.void, + resetShards: () => Effect.die("unexpected resetShards call") + } + + const storage = yield* MessageStorage.makeEncoded(encoded).pipe( + Effect.provide(Snowflake.layerGenerator) + ) + + const replies = yield* storage.repliesForUnfiltered([]) + expect(replies).toEqual([]) + + yield* storage.resetShards([]) + })) + }) }) export const GetUserRpc = Rpc.make("GetUser", { diff --git a/packages/cluster/test/Sharding.test.ts b/packages/cluster/test/Sharding.test.ts index c35215a21e1..bcde4d7d8d0 100644 --- a/packages/cluster/test/Sharding.test.ts +++ b/packages/cluster/test/Sharding.test.ts @@ -96,10 +96,14 @@ describe.concurrent("Sharding", () => { yield* TestClock.adjust(1) const config = yield* ShardingConfig.ShardingConfig ;(config as any).runnerAddress = Option.some(RunnerAddress.make("localhost", 1234)) - fiber.currentScheduler.scheduleTask(() => { - fiber.unsafeInterruptAsFork(FiberId.none) - Effect.runFork(testClock.adjust(30000)) - }, 0) + fiber.currentScheduler.scheduleTask( + () => { + fiber.unsafeInterruptAsFork(FiberId.none) + Effect.runFork(testClock.adjust(30000)) + }, + 0, + fiber + ) }).pipe( Effect.provide(TestShardingWithoutRunners.pipe( Layer.provide(Layer.scoped( diff --git a/packages/effect/CHANGELOG.md b/packages/effect/CHANGELOG.md index e263420705e..ef5d7003d64 100644 --- a/packages/effect/CHANGELOG.md +++ b/packages/effect/CHANGELOG.md @@ -1,5 +1,120 @@ # effect +## 3.21.2 + +### Patch Changes + +- [#6194](https://github.com/Effect-TS/effect/pull/6194) [`74f3267`](https://github.com/Effect-TS/effect/commit/74f3267a6cc7ed7818c4c34cc1232f7cfc7d3339) Thanks @mikearnaldi! - Fix `TestClock.unsafeCurrentTimeNanos()` to floor fractional millisecond instants before converting them to `BigInt`. + +## 3.21.1 + +### Patch Changes + +- [#6139](https://github.com/Effect-TS/effect/pull/6139) [`f99048e`](https://github.com/Effect-TS/effect/commit/f99048e9f4b89ce1afe31e1827dee5d751ddaa5b) Thanks @marbemac! - Fix batched request resolver defects causing consumer fibers to hang forever. + + When a `RequestResolver.makeBatched` resolver died with a defect, the request `Deferred`s were never completed because the cleanup logic in `invokeWithInterrupt` used `flatMap` (which only runs on success). Changed to `ensuring` so uncompleted request entries are always resolved regardless of exit type. + +## 3.21.0 + +### Minor Changes + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109) Thanks @kitlangton! - Add `Cron.prev` and reverse iteration support, aligning next/prev lookup tables, fixing DST handling symmetry, and expanding cron backward/forward test coverage. + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31) Thanks @mattiamanzati! - Add type-level utils to asserting layer types + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098) Thanks @schickling! - RcMap: support dynamic `idleTimeToLive` values per key + + The `idleTimeToLive` option can now be a function that receives the key and returns a duration, allowing different TTL values for different resources. + + ```ts + const map = + yield * + RcMap.make({ + lookup: (key: string) => acquireResource(key), + idleTimeToLive: (key: string) => { + if (key.startsWith("premium:")) return Duration.minutes(10) + return Duration.minutes(1) + } + }) + ``` + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb) Thanks @mikearnaldi! - Fix annotateCurrentSpan, add Effect.currentPropagatedSpan + +### Patch Changes + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb) Thanks @mikearnaldi! - Add logs to first propagated span, in the following case before this fix the log would not be added to the `p` span because `Effect.fn` adds a fake span for the purpose of adding a stack frame. + + ```ts + import { Effect } from "effect" + + const f = Effect.fn(function* () { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + ``` + +## 3.20.1 + +### Patch Changes + +- [#6133](https://github.com/Effect-TS/effect/pull/6133) [`add06f4`](https://github.com/Effect-TS/effect/commit/add06f4521403cbf4b9a692f9b59fb9d3d48293c) Thanks @aniravi24! - Fix `Equal.equals` crash when comparing `null` values inside `structuralRegion`. Added null guard before `Object.getPrototypeOf` calls to prevent `TypeError: Cannot convert undefined or null to object`. + +- [#6093](https://github.com/Effect-TS/effect/pull/6093) [`a03b6a2`](https://github.com/Effect-TS/effect/commit/a03b6a29ed0b983b0440b8ef4be47f47c57d73d7) Thanks @luchersou! - avoid class for PrettyError to preserve error.name + +## 3.20.0 + +### Minor Changes + +- [#6124](https://github.com/Effect-TS/effect/pull/6124) [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da) Thanks @mikearnaldi! - Fix scheduler task draining to isolate `AsyncLocalStorage` across fibers. + +### Patch Changes + +- [#6107](https://github.com/Effect-TS/effect/pull/6107) [`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54) Thanks @gcanti! - Backport `Types.VoidIfEmpty` to 3.x + +- [#6088](https://github.com/Effect-TS/effect/pull/6088) [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7) Thanks @taylorOntologize! - Schema: fix `Schema.omit` producing wrong result on Struct with `optionalWith({ default })` and index signatures + + `getIndexSignatures` now handles `Transformation` AST nodes by delegating to `ast.to`, matching the existing behavior of `getPropertyKeys` and `getPropertyKeyIndexedAccess`. Previously, `Schema.omit` on a struct combining `Schema.optionalWith` (with `{ default }`, `{ as: "Option" }`, etc.) and `Schema.Record` would silently take the wrong code path, returning a Transformation with property signatures instead of a TypeLiteral with index signatures. + +- [#6086](https://github.com/Effect-TS/effect/pull/6086) [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada) Thanks @taylorOntologize! - Schema: fix `getPropertySignatures` crash on Struct with `optionalWith({ default })` and other Transformation-producing variants + + `SchemaAST.getPropertyKeyIndexedAccess` now handles `Transformation` AST nodes by delegating to `ast.to`, matching the existing behavior of `getPropertyKeys`. Previously, calling `getPropertySignatures` on a `Schema.Struct` containing `Schema.optionalWith` with `{ default }`, `{ as: "Option" }`, `{ nullable: true }`, or similar options would throw `"Unsupported schema (Transformation)"`. + +- [#6097](https://github.com/Effect-TS/effect/pull/6097) [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2) Thanks @gcanti! - Fix TupleWithRest post-rest validation to check each tail index sequentially. + +## 3.19.19 + +### Patch Changes + +- [#6079](https://github.com/Effect-TS/effect/pull/6079) [`4eb5c00`](https://github.com/Effect-TS/effect/commit/4eb5c008dfc7d2a97b191ca608948d994a2cce4c) Thanks @tim-smart! - add short circuit to fiber.await internals + +- [#6079](https://github.com/Effect-TS/effect/pull/6079) [`4eb5c00`](https://github.com/Effect-TS/effect/commit/4eb5c008dfc7d2a97b191ca608948d994a2cce4c) Thanks @tim-smart! - build ManagedRuntime synchronously if possible + +- [#6081](https://github.com/Effect-TS/effect/pull/6081) [`2d2bb13`](https://github.com/Effect-TS/effect/commit/2d2bb1364a906bd44800c6387b9575fddccdaf53) Thanks @tim-smart! - fix semaphore race condition where permits could be leaked + +## 3.19.18 + +### Patch Changes + +- [#6062](https://github.com/Effect-TS/effect/pull/6062) [`12b1f1e`](https://github.com/Effect-TS/effect/commit/12b1f1eadf649e30dec581b7351ba3abb12f8004) Thanks @tim-smart! - prevent Stream.changes from writing empty chunks + +## 3.19.17 + +### Patch Changes + +- [#6040](https://github.com/Effect-TS/effect/pull/6040) [`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f) Thanks @jacobconley! - Fix `Stream.decodeText` to correctly handle multi-byte UTF-8 characters split across chunk boundaries. + +## 3.19.16 + +### Patch Changes + +- [#6018](https://github.com/Effect-TS/effect/pull/6018) [`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49) Thanks @codewithkenzo! - fix(Match): handle null/undefined in `Match.tag` and `Match.tagStartsWith` + + Added null checks to `discriminator` and `discriminatorStartsWith` predicates to prevent crashes when matching nullable union types. + + Fixes #6017 + ## 3.19.15 ### Patch Changes diff --git a/packages/effect/dtslint/Data.tst.ts b/packages/effect/dtslint/Data.tst.ts index 76a092476b4..f3db883328b 100644 --- a/packages/effect/dtslint/Data.tst.ts +++ b/packages/effect/dtslint/Data.tst.ts @@ -73,38 +73,6 @@ describe("Data", () => { expect(taggedPerson).type.toBe<(args: { readonly name: string; readonly optional?: string }) => TaggedPerson>() }) - it("TaggedEnum", () => { - type HttpError = Data.TaggedEnum<{ - BadRequest: { readonly status: 400; readonly a: string } - NotFound: { readonly status: 404; readonly b: number } - }> - expect>().type.toBe< - { readonly _tag: "BadRequest"; readonly status: 400; readonly a: string } - >() - expect>().type.toBe< - { readonly _tag: "NotFound"; readonly status: 404; readonly b: number } - >() - - // @ts-expect-error: It looks like you're trying to create a tagged enum, but one or more of its members already has a `_tag` property. - type _Err = Data.TaggedEnum<{ - A: { readonly _tag: "A" } - B: { readonly tag: "B" } - }> - }) - - it("taggedEnum", () => { - const { NotFound } = Data.taggedEnum< - | { readonly _tag: "BadRequest"; readonly status: 400; readonly message: string } - | { readonly _tag: "NotFound"; readonly status: 404; readonly message: string } - >() - - expect(NotFound).type.toBe< - ( - args: { readonly status: 404; readonly message: string } - ) => { readonly _tag: "NotFound"; readonly status: 404; readonly message: string } - >() - }) - it("Class", () => { class Person extends Data.Class<{ name: string; age?: number }> {} const person = new Person({ name: "Mike" }) @@ -161,4 +129,65 @@ describe("Data", () => { // void constructor expect>().type.toBe<[args?: void]>() }) + + describe("TaggedEnum", () => { + it("should be able to create a tagged enum", () => { + type TE = Data.TaggedEnum<{ + A: { readonly required: string } + B: { readonly optional?: number } + }> + expect>().type.toBe< + { readonly _tag: "A"; readonly required: string } + >() + expect>().type.toBe< + { readonly _tag: "B"; readonly optional?: number } + >() + }) + + it("should raise an error if one of the variants has a _tag property", () => { + // @ts-expect-error: It looks like you're trying to create a tagged enum, but one or more of its members already has a `_tag` property. + type TE = Data.TaggedEnum<{ + A: { readonly _tag: "A" } + B: { readonly b: number } + }> + }) + }) + + describe("taggedEnum", () => { + it("should be able to create a concrete tagged enum", () => { + type TE = Data.TaggedEnum<{ + A: { readonly required: string } + B: { readonly optional?: number } + }> + + const { $is, A, B } = Data.taggedEnum() + expect>().type.toBe<[{ readonly required: string }]>() + expect>().type.toBe<{ readonly _tag: "A"; readonly required: string }>() + expect>().type.toBe<[{ readonly optional?: number }]>() + expect>().type.toBe<{ readonly _tag: "B"; readonly optional?: number }>() + const isA = $is("A") + expect(isA).type.toBe< + (u: unknown) => u is { readonly _tag: "A"; readonly required: string } + >() + const isB = $is("B") + expect(isB).type.toBe< + (u: unknown) => u is { readonly _tag: "B"; readonly optional?: number } + >() + }) + + it("should be able to create a generic tagged enum", () => { + type TE = Data.TaggedEnum<{ + A: { a: T } + B: { b?: T } + }> + + interface TEDefinition extends Data.TaggedEnum.WithGenerics<1> { + readonly taggedEnum: TE + } + + const { A, B } = Data.taggedEnum() + expect().type.toBe<((args: { readonly a: A }) => { readonly _tag: "A"; readonly a: A })>() + expect().type.toBe<((args: { readonly b?: B }) => { readonly _tag: "B"; readonly b?: B })>() + }) + }) }) diff --git a/packages/effect/dtslint/Layer.tst.ts b/packages/effect/dtslint/Layer.tst.ts index 40168a49908..909927bcae5 100644 --- a/packages/effect/dtslint/Layer.tst.ts +++ b/packages/effect/dtslint/Layer.tst.ts @@ -1,4 +1,4 @@ -import { Layer, Schedule } from "effect" +import { Context, Layer, Schedule } from "effect" import { describe, expect, it } from "tstyche" interface In1 {} @@ -19,6 +19,8 @@ interface Out3 {} declare const layer3: Layer.Layer +class TestService1 extends Context.Tag("TestService1")() {} + describe("Layer", () => { it("merge", () => { expect(Layer.merge).type.not.toBeCallableWith() @@ -40,4 +42,28 @@ describe("Layer", () => { expect(Layer.retry(layer1, Schedule.recurs(1))).type.toBe>() expect(layer1.pipe(Layer.retry(Schedule.recurs(1)))).type.toBe>() }) + + it("ensureSuccessType", () => { + expect(layer1.pipe(Layer.ensureSuccessType())).type.toBe>() + }) + + it("ensureErrorType", () => { + const withoutError = Layer.succeed(TestService1, {}) + expect(withoutError.pipe(Layer.ensureErrorType())).type.toBe>() + + const withError = layer1 + expect(withError.pipe(Layer.ensureErrorType())).type.toBe>() + }) + + it("ensureRequirementsType", () => { + const withoutRequirements = Layer.succeed(TestService1, {}) + expect(withoutRequirements.pipe(Layer.ensureRequirementsType())).type.toBe< + Layer.Layer + >() + + const withRequirement = layer1 + expect(withRequirement.pipe(Layer.ensureRequirementsType())).type.toBe< + Layer.Layer + >() + }) }) diff --git a/packages/effect/package.json b/packages/effect/package.json index 8b6615fb142..45915143cd2 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -1,6 +1,6 @@ { "name": "effect", - "version": "3.19.15", + "version": "3.21.2", "type": "module", "license": "MIT", "description": "The missing standard library for TypeScript, for writing production-grade software.", diff --git a/packages/effect/src/Cron.ts b/packages/effect/src/Cron.ts index 99569d56a4f..43b16e9d28e 100644 --- a/packages/effect/src/Cron.ts +++ b/packages/effect/src/Cron.ts @@ -53,6 +53,15 @@ export interface Cron extends Pipeable, Equal.Equal, Inspectable { readonly weekday: number } /** @internal */ + readonly last: { + readonly second: number + readonly minute: number + readonly hour: number + readonly day: number + readonly month: number + readonly weekday: number + } + /** @internal */ readonly next: { readonly second: ReadonlyArray readonly minute: ReadonlyArray @@ -61,6 +70,15 @@ export interface Cron extends Pipeable, Equal.Equal, Inspectable { readonly month: ReadonlyArray readonly weekday: ReadonlyArray } + /** @internal */ + readonly prev: { + readonly second: ReadonlyArray + readonly minute: ReadonlyArray + readonly hour: ReadonlyArray + readonly day: ReadonlyArray + readonly month: ReadonlyArray + readonly weekday: ReadonlyArray + } } const CronProto = { @@ -151,31 +169,64 @@ export const make = (values: { weekday: weekdays[0] ?? 0 } + o.last = { + second: seconds[seconds.length - 1] ?? 59, + minute: minutes[minutes.length - 1] ?? 59, + hour: hours[hours.length - 1] ?? 23, + day: days[days.length - 1] ?? 31, + month: (months[months.length - 1] ?? 12) - 1, + weekday: weekdays[weekdays.length - 1] ?? 6 + } + o.next = { - second: nextLookupTable(seconds, 60), - minute: nextLookupTable(minutes, 60), - hour: nextLookupTable(hours, 24), - day: nextLookupTable(days, 32), - month: nextLookupTable(months, 13), - weekday: nextLookupTable(weekdays, 7) + second: lookupTable(seconds, 60, "next"), + minute: lookupTable(minutes, 60, "next"), + hour: lookupTable(hours, 24, "next"), + day: lookupTable(days, 32, "next"), + month: lookupTable(months, 13, "next"), + weekday: lookupTable(weekdays, 7, "next") + } + + o.prev = { + second: lookupTable(seconds, 60, "prev"), + minute: lookupTable(minutes, 60, "prev"), + hour: lookupTable(hours, 24, "prev"), + day: lookupTable(days, 32, "prev"), + month: lookupTable(months, 13, "prev"), + weekday: lookupTable(weekdays, 7, "prev") } return o } -const nextLookupTable = (values: ReadonlyArray, size: number): Array => { +const lookupTable = ( + values: ReadonlyArray, + size: number, + dir: "next" | "prev" +): Array => { const result = new Array(size).fill(undefined) if (values.length === 0) { return result } let current: number | undefined = undefined - let index = values.length - 1 - for (let i = size - 1; i >= 0; i--) { - while (index >= 0 && values[index] >= i) { - current = values[index--] + + if (dir === "next") { + let index = values.length - 1 + for (let i = size - 1; i >= 0; i--) { + while (index >= 0 && values[index] >= i) { + current = values[index--] + } + result[i] = current + } + } else { + let index = 0 + for (let i = 0; i < size; i++) { + while (index < values.length && values[index] <= i) { + current = values[index++] + } + result[i] = current } - result[i] = current } return result @@ -376,7 +427,7 @@ const daysInMonth = (date: Date): number => /** * Returns the next run `Date` for the given `Cron` instance. * - * Uses the current time as a starting point if no value is provided for `now`. + * Uses the current time as a starting point if no value is provided for `startFrom`. * * @example * ```ts @@ -394,38 +445,76 @@ const daysInMonth = (date: Date): number => * @since 2.0.0 */ export const next = (cron: Cron, startFrom?: DateTime.DateTime.Input): Date => { + return stepCron(cron, startFrom, "next") +} + +/** + * Returns the previous run `Date` for the given `Cron` instance. + * + * Uses the current time as a starting point if no value is provided for `startFrom`. + * + * @example + * ```ts + * import * as assert from "node:assert" + * import { Cron, Either } from "effect" + * + * const before = new Date("2021-01-15 00:00:00") + * const cron = Either.getOrThrow(Cron.parse("0 4 8-14 * *")) + * assert.deepStrictEqual(Cron.prev(cron, before), new Date("2021-01-14 04:00:00")) + * ``` + * + * @throws `IllegalArgumentException` if the given `DateTime.Input` is invalid. + * @throws `Error` if the previous run date cannot be found within 10,000 iterations. + * + * @since 3.20.0 + */ +export const prev = (cron: Cron, startFrom?: DateTime.DateTime.Input): Date => { + return stepCron(cron, startFrom, "prev") +} + +/** @internal */ +const stepCron = (cron: Cron, startFrom: DateTime.DateTime.Input | undefined, direction: "next" | "prev"): Date => { const tz = Option.getOrUndefined(cron.tz) const zoned = dateTime.unsafeMakeZoned(startFrom ?? new Date(), { timeZone: tz }) + const prev = direction === "prev" + const tick = prev ? -1 : 1 + const table = cron[direction] + const boundary = prev ? cron.last : cron.first + + const needsStep = prev + ? (next: number, current: number) => next < current + : (next: number, current: number) => next > current + const utc = tz !== undefined && dateTime.isTimeZoneNamed(tz) && tz.id === "UTC" const adjustDst = utc ? constVoid : (current: Date) => { const adjusted = dateTime.unsafeMakeZoned(current, { timeZone: zoned.zone, - adjustForTimeZone: true + adjustForTimeZone: true, + disambiguation: prev ? "later" : undefined }).pipe(dateTime.toDate) - // TODO: This implementation currently only skips forward when transitioning into daylight savings time. const drift = current.getTime() - adjusted.getTime() - if (drift > 0) { - current.setTime(current.getTime() + drift) + if (prev ? drift !== 0 : drift > 0) { + current.setTime(adjusted.getTime()) } } const result = dateTime.mutate(zoned, (current) => { - current.setUTCSeconds(current.getUTCSeconds() + 1, 0) + current.setUTCSeconds(current.getUTCSeconds() + tick, 0) for (let i = 0; i < 10_000; i++) { if (cron.seconds.size !== 0) { const currentSecond = current.getUTCSeconds() - const nextSecond = cron.next.second[currentSecond] + const nextSecond = table.second[currentSecond] if (nextSecond === undefined) { - current.setUTCMinutes(current.getUTCMinutes() + 1, cron.first.second) + current.setUTCMinutes(current.getUTCMinutes() + tick, boundary.second) adjustDst(current) continue } - if (nextSecond > currentSecond) { + if (needsStep(nextSecond, currentSecond)) { current.setUTCSeconds(nextSecond) adjustDst(current) continue @@ -434,14 +523,14 @@ export const next = (cron: Cron, startFrom?: DateTime.DateTime.Input): Date => { if (cron.minutes.size !== 0) { const currentMinute = current.getUTCMinutes() - const nextMinute = cron.next.minute[currentMinute] + const nextMinute = table.minute[currentMinute] if (nextMinute === undefined) { - current.setUTCHours(current.getUTCHours() + 1, cron.first.minute, cron.first.second) + current.setUTCHours(current.getUTCHours() + tick, boundary.minute, boundary.second) adjustDst(current) continue } - if (nextMinute > currentMinute) { - current.setUTCMinutes(nextMinute, cron.first.second) + if (needsStep(nextMinute, currentMinute)) { + current.setUTCMinutes(nextMinute, boundary.second) adjustDst(current) continue } @@ -449,40 +538,61 @@ export const next = (cron: Cron, startFrom?: DateTime.DateTime.Input): Date => { if (cron.hours.size !== 0) { const currentHour = current.getUTCHours() - const nextHour = cron.next.hour[currentHour] + const nextHour = table.hour[currentHour] if (nextHour === undefined) { - current.setUTCDate(current.getUTCDate() + 1) - current.setUTCHours(cron.first.hour, cron.first.minute, cron.first.second) + current.setUTCDate(current.getUTCDate() + tick) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) adjustDst(current) continue } - if (nextHour > currentHour) { - current.setUTCHours(nextHour, cron.first.minute, cron.first.second) + if (needsStep(nextHour, currentHour)) { + current.setUTCHours(nextHour, boundary.minute, boundary.second) adjustDst(current) continue } } if (cron.weekdays.size !== 0 || cron.days.size !== 0) { - let a: number = Infinity - let b: number = Infinity + let a: number = prev ? -Infinity : Infinity + let b: number = prev ? -Infinity : Infinity if (cron.weekdays.size !== 0) { const currentWeekday = current.getUTCDay() - const nextWeekday = cron.next.weekday[currentWeekday] - a = nextWeekday === undefined ? 7 - currentWeekday + cron.first.weekday : nextWeekday - currentWeekday + const nextWeekday = table.weekday[currentWeekday] + if (nextWeekday === undefined) { + a = prev + ? currentWeekday - 7 + boundary.weekday + : 7 - currentWeekday + boundary.weekday + } else { + a = nextWeekday - currentWeekday + } } + // Only check day-of-month if weekday constraint not already satisfied (they're OR'd) if (cron.days.size !== 0 && a !== 0) { const currentDay = current.getUTCDate() - const nextDay = cron.next.day[currentDay] - b = nextDay === undefined ? daysInMonth(current) - currentDay + cron.first.day : nextDay - currentDay + const nextDay = table.day[currentDay] + if (nextDay === undefined) { + if (prev) { + // When wrapping to previous month, calculate days back: + // Current day offset + gap from end of prev month to target day + // Example: June 3 → May 20 with boundary.day=20: -(3 + (31 - 20)) = -14 + const prevMonthDays = daysInMonth( + new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), 0)) + ) + b = -(currentDay + (prevMonthDays - boundary.day)) + } else { + b = daysInMonth(current) - currentDay + boundary.day + } + } else { + b = nextDay - currentDay + } } - const addDays = Math.min(a, b) + const addDays = prev ? Math.max(a, b) : Math.min(a, b) if (addDays !== 0) { current.setUTCDate(current.getUTCDate() + addDays) - current.setUTCHours(cron.first.hour, cron.first.minute, cron.first.second) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) adjustDst(current) continue } @@ -490,17 +600,25 @@ export const next = (cron: Cron, startFrom?: DateTime.DateTime.Input): Date => { if (cron.months.size !== 0) { const currentMonth = current.getUTCMonth() + 1 - const nextMonth = cron.next.month[currentMonth] + const nextMonth = table.month[currentMonth] + const clampBoundaryDay = (targetMonthIndex: number): number => { + if (cron.days.size !== 0) { + return boundary.day + } + const maxDayInMonth = daysInMonth(new Date(Date.UTC(current.getUTCFullYear(), targetMonthIndex, 1))) + return Math.min(boundary.day, maxDayInMonth) + } if (nextMonth === undefined) { - current.setUTCFullYear(current.getUTCFullYear() + 1) - current.setUTCMonth(cron.first.month, cron.first.day) - current.setUTCHours(cron.first.hour, cron.first.minute, cron.first.second) + current.setUTCFullYear(current.getUTCFullYear() + tick) + current.setUTCMonth(boundary.month, clampBoundaryDay(boundary.month)) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) adjustDst(current) continue } - if (nextMonth > currentMonth) { - current.setUTCMonth(nextMonth - 1, cron.first.day) - current.setUTCHours(cron.first.hour, cron.first.minute, cron.first.second) + if (needsStep(nextMonth, currentMonth)) { + const targetMonthIndex = nextMonth - 1 + current.setUTCMonth(targetMonthIndex, clampBoundaryDay(targetMonthIndex)) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) adjustDst(current) continue } @@ -526,6 +644,18 @@ export const sequence = function*(cron: Cron, startFrom?: DateTime.DateTime.Inpu } } +/** + * Returns an `IterableIterator` which yields the sequence of `Date`s that match the `Cron` instance, + * in reverse direction. + * + * @since 3.20.0 + */ +export const sequenceReverse = function*(cron: Cron, startFrom?: DateTime.DateTime.Input): IterableIterator { + while (true) { + yield startFrom = prev(cron, startFrom) + } +} + /** * @category instances * @since 2.0.0 diff --git a/packages/effect/src/Data.ts b/packages/effect/src/Data.ts index 930c46bd7fe..f4910d487fe 100644 --- a/packages/effect/src/Data.ts +++ b/packages/effect/src/Data.ts @@ -19,8 +19,7 @@ export declare namespace Case { */ export interface Constructor { ( - args: Types.Equals, {}> extends true ? void - : { readonly [P in keyof A as P extends Tag ? never : P]: A[P] } + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends Tag ? never : P]: A[P] }> ): A } } @@ -202,8 +201,7 @@ export const tagged = ( * @category constructors */ export const Class: new = {}>( - args: Types.Equals extends true ? void - : { readonly [P in keyof A]: A[P] } + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> ) => Readonly = internal.Structural as any /** @@ -234,8 +232,7 @@ export const Class: new = {}>( export const TaggedClass = ( tag: Tag ): new = {}>( - args: Types.Equals extends true ? void - : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] } + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> ) => Readonly & { readonly _tag: Tag } => { class Base extends Class { readonly _tag = tag @@ -248,8 +245,7 @@ export const TaggedClass = ( * @category constructors */ export const Structural: new( - args: Types.Equals extends true ? void - : { readonly [P in keyof A]: A[P] } + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> ) => {} = internal.Structural as any /** @@ -339,7 +335,7 @@ export declare namespace TaggedEnum { A extends { readonly _tag: string }, K extends A["_tag"], E = Extract - > = { readonly [K in keyof E as K extends "_tag" ? never : K]: E[K] } extends infer T ? {} extends T ? void : T + > = { readonly [K in keyof E as K extends "_tag" ? never : K]: E[K] } extends infer T ? Types.VoidIfEmpty : never /** @@ -556,8 +552,7 @@ function taggedMatch< * @category constructors */ export const Error: new = {}>( - args: Types.Equals extends true ? void - : { readonly [P in keyof A]: A[P] } + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> ) => Cause.YieldableError & Readonly = (function() { const plainArgsSymbol = Symbol.for("effect/Data/Error/plainArgs") const O = { @@ -583,8 +578,7 @@ export const Error: new = {}>( * @category constructors */ export const TaggedError = (tag: Tag): new = {}>( - args: Types.Equals extends true ? void - : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] } + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> ) => Cause.YieldableError & { readonly _tag: Tag } & Readonly => { const O = { BaseEffectError: class extends Error<{}> { diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index b1e897d82b1..d9143c239b6 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -12998,6 +12998,12 @@ export const annotateCurrentSpan: { */ export const currentSpan: Effect = effect.currentSpan +/** + * @since 3.20.0 + * @category Tracing + */ +export const currentPropagatedSpan: Effect = effect.currentPropagatedSpan + /** * @since 2.0.0 * @category Tracing diff --git a/packages/effect/src/Equal.ts b/packages/effect/src/Equal.ts index 5e2738d95f9..e6ca6d8ba23 100644 --- a/packages/effect/src/Equal.ts +++ b/packages/effect/src/Equal.ts @@ -60,6 +60,9 @@ function compareBoth(self: unknown, that: unknown): boolean { } } if (structuralRegionState.enabled) { + if (self === null || that === null) { + return false + } if (Array.isArray(self) && Array.isArray(that)) { return self.length === that.length && self.every((v, i) => compareBoth(v, that[i])) } diff --git a/packages/effect/src/Layer.ts b/packages/effect/src/Layer.ts index 82aa92f6fa6..945e365001c 100644 --- a/packages/effect/src/Layer.ts +++ b/packages/effect/src/Layer.ts @@ -735,7 +735,7 @@ export const scoped: { } = internal.scoped /** - * Constructs a layer from the specified scoped effect. + * Constructs a layer from the specified scoped effect, discarding its output. * * @since 2.0.0 * @category constructors @@ -1226,3 +1226,55 @@ export const updateService = dual< layer, map(context(), (c) => Context.add(c, tag, f(Context.unsafeGet(c, tag)))) )) + +// ----------------------------------------------------------------------------- +// Type constraints +// ----------------------------------------------------------------------------- + +/** + * A no-op type constraint that enforces the success channel of a Layer conforms to + * the specified success type `ROut`. + * + * @example + * import { Layer } from "effect" + * + * // Ensure that the layer produces the expected services. + * const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureSuccessType()) + * + * @since 3.20.0 + * @category Type constraints + */ +export const ensureSuccessType = + () => (layer: Layer): Layer => layer + +/** + * A no-op type constraint that enforces the error channel of a Layer conforms to + * the specified error type `E`. + * + * @example + * import { Layer } from "effect" + * + * // Ensure that the layer does not expose any unhandled errors. + * const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureErrorType()) + * + * @since 3.20.0 + * @category Type constraints + */ +export const ensureErrorType = () => (layer: Layer): Layer => + layer + +/** + * A no-op type constraint that enforces the requirements channel of a Layer conforms to + * the specified requirements type `RIn`. + * + * @example + * import { Layer } from "effect" + * + * // Ensure that the layer does not have any requirements. + * const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureRequirementsType()) + * + * @since 3.20.0 + * @category Type constraints + */ +export const ensureRequirementsType = + () => (layer: Layer): Layer => layer diff --git a/packages/effect/src/ParseResult.ts b/packages/effect/src/ParseResult.ts index b85a377aeaf..c89f8295073 100644 --- a/packages/effect/src/ParseResult.ts +++ b/packages/effect/src/ParseResult.ts @@ -1033,15 +1033,15 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { // handle post rest elements // --------------------------------------------- for (let j = 0; j < tail.length; j++) { - i += j - if (len < i + 1) { + const index = i + j + if (len < index + 1) { continue } else { - const te = tail[j](input[i], options) + const te = tail[j](input[index], options) if (isEither(te)) { if (Either.isLeft(te)) { // the input element is present but is not valid - const e = new Pointer(i, input, te.left) + const e = new Pointer(index, input, te.left) if (allErrors) { es.push([stepKey++, e]) continue @@ -1052,7 +1052,6 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { output.push([stepKey++, te.right]) } else { const nk = stepKey++ - const index = i if (!queue) { queue = [] } diff --git a/packages/effect/src/RcMap.ts b/packages/effect/src/RcMap.ts index efd93797083..9e78cdce1f9 100644 --- a/packages/effect/src/RcMap.ts +++ b/packages/effect/src/RcMap.ts @@ -56,6 +56,7 @@ export declare namespace RcMap { * * - `capacity`: The maximum number of resources that can be held in the map. * - `idleTimeToLive`: When the reference count reaches zero, the resource will be released after this duration. + * Can be a static duration or a function that returns a duration based on the key. * * @since 3.5.0 * @category models @@ -85,14 +86,14 @@ export const make: { ( options: { readonly lookup: (key: K) => Effect.Effect - readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined readonly capacity?: undefined } ): Effect.Effect, never, Scope.Scope | R> ( options: { readonly lookup: (key: K) => Effect.Effect - readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined readonly capacity: number } ): Effect.Effect, never, Scope.Scope | R> diff --git a/packages/effect/src/Scheduler.ts b/packages/effect/src/Scheduler.ts index 853d02b98a4..68d054300a0 100644 --- a/packages/effect/src/Scheduler.ts +++ b/packages/effect/src/Scheduler.ts @@ -21,7 +21,69 @@ export type Task = () => void */ export interface Scheduler { shouldYield(fiber: RuntimeFiber): number | false - scheduleTask(task: Task, priority: number): void + scheduleTask(task: Task, priority: number, fiber?: RuntimeFiber): void +} + +/** + * @since 3.20.0 + * @category models + */ +export class SchedulerRunner { + running = false + tasks = new PriorityBuckets() + + constructor( + readonly scheduleDrain: (depth: number, drain: (depth: number) => void) => void + ) {} + + private starveInternal = (depth: number) => { + const tasks = this.tasks.buckets + this.tasks.buckets = [] + for (const [_, toRun] of tasks) { + for (let i = 0; i < toRun.length; i++) { + toRun[i]() + } + } + if (this.tasks.buckets.length === 0) { + this.running = false + } else { + this.starve(depth) + } + } + + private starve(depth = 0) { + this.scheduleDrain(depth, this.starveInternal) + } + + scheduleTask(task: Task, priority: number) { + this.tasks.scheduleTask(task, priority) + if (!this.running) { + this.running = true + this.starve() + } + } + /** + * @since 3.20.0 + * @category constructors + */ + static cached( + scheduleDrain: (depth: number, drain: (depth: number) => void) => void + ) { + const fallback = new SchedulerRunner(scheduleDrain) + const runners = new WeakMap, SchedulerRunner>() + + return (fiber?: RuntimeFiber) => { + if (fiber === undefined) { + return fallback + } + let runner = runners.get(fiber) + if (runner === undefined) { + runner = new SchedulerRunner(scheduleDrain) + runners.set(fiber, runner) + } + return runner + } + } } /** @@ -62,14 +124,13 @@ export class PriorityBuckets { * @category constructors */ export class MixedScheduler implements Scheduler { - /** - * @since 2.0.0 - */ - running = false - /** - * @since 2.0.0 - */ - tasks = new PriorityBuckets() + private readonly getRunner = SchedulerRunner.cached((depth, drain) => { + if (depth >= this.maxNextTickBeforeTimer) { + setTimeout(() => drain(0), 0) + } else { + Promise.resolve(void 0).then(() => drain(depth + 1)) + } + }) constructor( /** @@ -78,35 +139,6 @@ export class MixedScheduler implements Scheduler { readonly maxNextTickBeforeTimer: number ) {} - /** - * @since 2.0.0 - */ - private starveInternal(depth: number) { - const tasks = this.tasks.buckets - this.tasks.buckets = [] - for (const [_, toRun] of tasks) { - for (let i = 0; i < toRun.length; i++) { - toRun[i]() - } - } - if (this.tasks.buckets.length === 0) { - this.running = false - } else { - this.starve(depth) - } - } - - /** - * @since 2.0.0 - */ - private starve(depth = 0) { - if (depth >= this.maxNextTickBeforeTimer) { - setTimeout(() => this.starveInternal(0), 0) - } else { - Promise.resolve(void 0).then(() => this.starveInternal(depth + 1)) - } - } - /** * @since 2.0.0 */ @@ -119,12 +151,8 @@ export class MixedScheduler implements Scheduler { /** * @since 2.0.0 */ - scheduleTask(task: Task, priority: number) { - this.tasks.scheduleTask(task, priority) - if (!this.running) { - this.running = true - this.starve() - } + scheduleTask(task: Task, priority: number, fiber?: RuntimeFiber) { + this.getRunner(fiber).scheduleTask(task, priority) } } @@ -155,9 +183,9 @@ export class SyncScheduler implements Scheduler { /** * @since 2.0.0 */ - scheduleTask(task: Task, priority: number) { + scheduleTask(task: Task, priority: number, fiber?: RuntimeFiber) { if (this.deferred) { - defaultScheduler.scheduleTask(task, priority) + defaultScheduler.scheduleTask(task, priority, fiber) } else { this.tasks.scheduleTask(task, priority) } @@ -207,9 +235,9 @@ export class ControlledScheduler implements Scheduler { /** * @since 2.0.0 */ - scheduleTask(task: Task, priority: number) { + scheduleTask(task: Task, priority: number, fiber?: RuntimeFiber) { if (this.deferred) { - defaultScheduler.scheduleTask(task, priority) + defaultScheduler.scheduleTask(task, priority, fiber) } else { this.tasks.scheduleTask(task, priority) } @@ -254,16 +282,16 @@ export const makeMatrix = (...record: Array<[number, Scheduler]>): Scheduler => } return false }, - scheduleTask(task, priority) { + scheduleTask(task, priority, fiber) { let scheduler: Scheduler | undefined = undefined for (const i of index) { if (priority >= i[0]) { scheduler = i[1] } else { - return (scheduler ?? defaultScheduler).scheduleTask(task, priority) + return (scheduler ?? defaultScheduler).scheduleTask(task, priority, fiber) } } - return (scheduler ?? defaultScheduler).scheduleTask(task, priority) + return (scheduler ?? defaultScheduler).scheduleTask(task, priority, fiber) } } } @@ -298,31 +326,12 @@ export const makeBatched = ( callback: (runBatch: () => void) => void, shouldYield: Scheduler["shouldYield"] = defaultShouldYield ) => { - let running = false - const tasks = new PriorityBuckets() - const starveInternal = () => { - const tasksToRun = tasks.buckets - tasks.buckets = [] - for (const [_, toRun] of tasksToRun) { - for (let i = 0; i < toRun.length; i++) { - toRun[i]() - } - } - if (tasks.buckets.length === 0) { - running = false - } else { - starve() - } - } - - const starve = () => callback(starveInternal) + const getRunner = SchedulerRunner.cached((_, drain) => { + callback(() => drain(0)) + }) - return make((task, priority) => { - tasks.scheduleTask(task, priority) - if (!running) { - running = true - starve() - } + return make((task, priority, fiber) => { + getRunner(fiber).scheduleTask(task, priority) }, shouldYield) } diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index db74f0387a6..08719a018d5 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -5415,7 +5415,7 @@ export type JsonNumberSchemaId = typeof JsonNumberSchemaId * import * as assert from "node:assert" * import * as Schema from "effect/Schema" * - * const is = Schema.is(S.JsonNumber) + * const is = Schema.is(Schema.JsonNumber) * * assert.deepStrictEqual(is(42), true) * assert.deepStrictEqual(is(Number.NaN), false) diff --git a/packages/effect/src/SchemaAST.ts b/packages/effect/src/SchemaAST.ts index 85f9ec8c42a..1f9efb8a2d0 100644 --- a/packages/effect/src/SchemaAST.ts +++ b/packages/effect/src/SchemaAST.ts @@ -2231,6 +2231,8 @@ const getIndexSignatures = (ast: AST): Array => { return getIndexSignatures(ast.f()) case "Refinement": return getIndexSignatures(ast.from) + case "Transformation": + return getIndexSignatures(ast.to) } return [] } @@ -2328,6 +2330,8 @@ export const getPropertyKeyIndexedAccess = (ast: AST, name: PropertyKey): Proper return getPropertyKeyIndexedAccess(ast.f(), name) case "Refinement": return getPropertyKeyIndexedAccess(ast.from, name) + case "Transformation": + return getPropertyKeyIndexedAccess(ast.to, name) } throw new Error(errors_.getASTUnsupportedSchemaErrorMessage(ast)) } diff --git a/packages/effect/src/TestClock.ts b/packages/effect/src/TestClock.ts index 3e64d78c42c..81bf45e1970 100644 --- a/packages/effect/src/TestClock.ts +++ b/packages/effect/src/TestClock.ts @@ -158,7 +158,7 @@ export class TestClockImpl implements TestClock { * Unsafely returns the current time in nanoseconds. */ unsafeCurrentTimeNanos(): bigint { - return BigInt(ref.unsafeGet(this.clockState).instant * 1000000) + return BigInt(Math.floor(this.unsafeCurrentTimeMillis() * 1000000)) } /** diff --git a/packages/effect/src/Types.ts b/packages/effect/src/Types.ts index 1b1dd40afd9..90d6476553c 100644 --- a/packages/effect/src/Types.ts +++ b/packages/effect/src/Types.ts @@ -351,3 +351,11 @@ export type NoExcessProperties = T & { readonly [K in Exclude = new(...args: Array) => T + +/** + * Conditional type that returns `void` if `S` is an empty object type, + * otherwise returns `S`. + * + * @since 3.19.20 + */ +export type VoidIfEmpty = keyof S extends never ? void : S diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index 881ef5b6d73..e557a914235 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -14,7 +14,7 @@ import { pipeArguments } from "../Pipeable.js" import type { Predicate, Refinement } from "../Predicate.js" import { hasProperty, isFunction } from "../Predicate.js" import type { AnySpan, Span } from "../Tracer.js" -import type { NoInfer } from "../Types.js" +import type * as Types from "../Types.js" import { getBugErrorMessage } from "./errors.js" import * as OpCodes from "./opCodes/cause.js" @@ -879,59 +879,58 @@ export const pretty = (cause: Cause.Cause, options?: { if (options?.renderErrorCause !== true || e.cause === undefined) { return e.stack } - return `${e.stack} {\n${renderErrorCause(e.cause as PrettyError, " ")}\n}` + return `${e.stack} {\n${renderErrorCause(e.cause as Cause.PrettyError, " ")}\n}` }).join("\n") } -const renderErrorCause = (cause: PrettyError, prefix: string) => { +const renderErrorCause = (cause: Cause.PrettyError, prefix: string) => { const lines = cause.stack!.split("\n") let stack = `${prefix}[cause]: ${lines[0]}` for (let i = 1, len = lines.length; i < len; i++) { stack += `\n${prefix}${lines[i]}` } if (cause.cause) { - stack += ` {\n${renderErrorCause(cause.cause as PrettyError, `${prefix} `)}\n${prefix}}` + stack += ` {\n${renderErrorCause(cause.cause as Cause.PrettyError, `${prefix} `)}\n${prefix}}` } return stack } /** @internal */ -export class PrettyError extends globalThis.Error implements Cause.PrettyError { - span: undefined | Span = undefined - constructor(originalError: unknown) { - const originalErrorIsObject = typeof originalError === "object" && originalError !== null - const prevLimit = Error.stackTraceLimit - Error.stackTraceLimit = 1 - super( - prettyErrorMessage(originalError), - originalErrorIsObject && "cause" in originalError && typeof originalError.cause !== "undefined" - ? { cause: new PrettyError(originalError.cause) } - : undefined - ) - if (this.message === "") { - this.message = "An error has occurred" +export const makePrettyError = (originalError: unknown): Cause.PrettyError => { + const originalErrorIsObject = typeof originalError === "object" && originalError !== null + const prevLimit = Error.stackTraceLimit + Error.stackTraceLimit = 1 + const error = new Error( + prettyErrorMessage(originalError), + originalErrorIsObject && "cause" in originalError && typeof originalError.cause !== "undefined" + ? { cause: makePrettyError(originalError.cause) } + : undefined + ) as Types.Mutable + Error.stackTraceLimit = prevLimit + if (error.message === "") { + error.message = "An error has occurred" + } + Error.stackTraceLimit = prevLimit + error.name = originalError instanceof Error ? originalError.name : "Error" + if (originalErrorIsObject) { + if (spanSymbol in originalError) { + error.span = originalError[spanSymbol] as Span } - Error.stackTraceLimit = prevLimit - this.name = originalError instanceof Error ? originalError.name : "Error" - if (originalErrorIsObject) { - if (spanSymbol in originalError) { - this.span = originalError[spanSymbol] as Span + Object.keys(originalError).forEach((key) => { + if (!(key in error)) { + // @ts-expect-error + error[key] = originalError[key] } - Object.keys(originalError).forEach((key) => { - if (!(key in this)) { - // @ts-expect-error - this[key] = originalError[key] - } - }) - } - this.stack = prettyErrorStack( - `${this.name}: ${this.message}`, - originalError instanceof Error && originalError.stack - ? originalError.stack - : "", - this.span - ) + }) } + error.stack = prettyErrorStack( + `${error.name}: ${error.message}`, + originalError instanceof Error && originalError.stack + ? originalError.stack + : "", + error.span + ) + return error } /** @@ -1035,14 +1034,14 @@ const prettyErrorStack = (message: string, stack: string, span?: Span | undefine export const spanSymbol = Symbol.for("effect/SpanAnnotation") /** @internal */ -export const prettyErrors = (cause: Cause.Cause): Array => +export const prettyErrors = (cause: Cause.Cause): Array => reduceWithContext(cause, void 0, { - emptyCase: (): Array => [], + emptyCase: (): Array => [], dieCase: (_, unknownError) => { - return [new PrettyError(unknownError)] + return [makePrettyError(unknownError)] }, failCase: (_, error) => { - return [new PrettyError(error)] + return [makePrettyError(error)] }, interruptCase: () => [], parallelCase: (_, l, r) => [...l, ...r], diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index 2b90e1d0068..0ef68fb67b6 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -1966,7 +1966,7 @@ export const annotateCurrentSpan: { } = function(): Effect.Effect { const args = arguments return ignore(core.flatMap( - currentSpan, + currentPropagatedSpan, (span) => core.sync(() => { if (typeof args[0] === "string") { @@ -2041,6 +2041,16 @@ export const currentSpan: Effect.Effect = core.flatMap( + core.context(), + (context) => { + const span = filterDisablePropagation(Context.getOption(context, internalTracer.spanTag)) + return span._tag === "Some" && span.value._tag === "Span" + ? core.succeed(span.value) + : core.fail(new core.NoSuchElementException()) + } +) + /* @internal */ export const linkSpans = dual< ( @@ -2070,12 +2080,13 @@ export const linkSpans = dual< const bigint0 = BigInt(0) -const filterDisablePropagation: (self: Option.Option) => Option.Option = Option.flatMap( - (span) => - Context.get(span.context, internalTracer.DisablePropagation) - ? span._tag === "Span" ? filterDisablePropagation(span.parent) : Option.none() - : Option.some(span) -) +export const filterDisablePropagation: (self: Option.Option) => Option.Option = Option + .flatMap( + (span) => + Context.get(span.context, internalTracer.DisablePropagation) + ? span._tag === "Span" ? filterDisablePropagation(span.parent) : Option.none() + : Option.some(span) + ) /** @internal */ export const unsafeMakeSpan = ( diff --git a/packages/effect/src/internal/effect/circular.ts b/packages/effect/src/internal/effect/circular.ts index 67fade305a0..e64ea05d880 100644 --- a/packages/effect/src/internal/effect/circular.ts +++ b/packages/effect/src/internal/effect/circular.ts @@ -47,33 +47,41 @@ class Semaphore { core.asyncInterrupt((resume) => { if (this.free < n) { const observer = () => { - if (this.free < n) { - return - } + if (this.free < n) return this.waiters.delete(observer) - this.taken += n - resume(core.succeed(n)) + resume(core.suspend(() => { + if (this.free < n) return this.take(n) + this.taken += n + return core.succeed(n) + })) } this.waiters.add(observer) return core.sync(() => { this.waiters.delete(observer) }) } - this.taken += n - return resume(core.succeed(n)) + resume(core.suspend(() => { + if (this.free < n) return this.take(n) + this.taken += n + return core.succeed(n) + })) }) updateTakenUnsafe(fiber: Fiber.RuntimeFiber, f: (n: number) => number): Effect.Effect { this.taken = f(this.taken) if (this.waiters.size > 0) { - fiber.getFiberRef(currentScheduler).scheduleTask(() => { - const iter = this.waiters.values() - let item = iter.next() - while (item.done === false && this.free > 0) { - item.value() - item = iter.next() - } - }, fiber.getFiberRef(core.currentSchedulingPriority)) + fiber.getFiberRef(currentScheduler).scheduleTask( + () => { + const iter = this.waiters.values() + let item = iter.next() + while (item.done === false && this.free > 0) { + item.value() + item = iter.next() + } + }, + fiber.getFiberRef(core.currentSchedulingPriority), + fiber + ) } return core.succeed(this.free) } @@ -139,7 +147,7 @@ class Latch extends Effectable.Class implements Effect.Latch { return core.void } this.scheduled = true - fiber.currentScheduler.scheduleTask(this.flushWaiters, fiber.getFiberRef(core.currentSchedulingPriority)) + fiber.currentScheduler.scheduleTask(this.flushWaiters, fiber.getFiberRef(core.currentSchedulingPriority), fiber) return core.void } private flushWaiters = () => { diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index 8362eb2b34a..f93e75c92f9 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -451,6 +451,10 @@ export class FiberRuntime extends Effectable.Class> { return core.async((resume) => { const cb = (exit: Exit.Exit) => resume(core.succeed(exit)) + if (this._exitValue !== null) { + cb(this._exitValue!) + return + } this.tell( FiberMessage.stateful((fiber, _) => { if (fiber._exitValue !== null) { @@ -704,7 +708,8 @@ export class FiberRuntime extends Effectable.Class { - const span = Context.getOption( + const span = internalEffect.filterDisablePropagation(Context.getOption( fiberRefs.getOrDefault(context, core.currentContext), tracer.spanTag - ) + )) + if (span._tag === "None" || span.value._tag === "ExternalSpan") { return } + const clockService = Context.unsafeGet( fiberRefs.getOrDefault(context, defaultServices.currentServices), clock.clockTag @@ -2205,9 +2212,13 @@ export const forEachConcurrentDiscard = ( const results = new Array() const interruptAll = () => fibers.forEach((fiber) => { - fiber.currentScheduler.scheduleTask(() => { - fiber.unsafeInterruptAsFork(parent.id()) - }, 0) + fiber.currentScheduler.scheduleTask( + () => { + fiber.unsafeInterruptAsFork(parent.id()) + }, + 0, + fiber + ) }) const startOrder = new Array | Effect.Blocked>>() const joinOrder = new Array | Effect.Blocked>>() @@ -2230,12 +2241,16 @@ export const forEachConcurrentDiscard = ( parent.currentRuntimeFlags, fiberScope.globalScope ) - parent.currentScheduler.scheduleTask(() => { - if (interruptImmediately) { - fiber.unsafeInterruptAsFork(parent.id()) - } - fiber.resume(runnable) - }, 0) + parent.currentScheduler.scheduleTask( + () => { + if (interruptImmediately) { + fiber.unsafeInterruptAsFork(parent.id()) + } + fiber.resume(runnable) + }, + 0, + fiber + ) return fiber } const onInterruptSignal = () => { @@ -2291,9 +2306,13 @@ export const forEachConcurrentDiscard = ( startOrder.push(fiber) fibers.add(fiber) if (interrupted) { - fiber.currentScheduler.scheduleTask(() => { - fiber.unsafeInterruptAsFork(parent.id()) - }, 0) + fiber.currentScheduler.scheduleTask( + () => { + fiber.unsafeInterruptAsFork(parent.id()) + }, + 0, + fiber + ) } fiber.addObserver((wrapped) => { let exit: Exit.Exit | core.Blocked @@ -3703,7 +3722,7 @@ export const invokeWithInterrupt: ( onInterrupt?: () => void ) => core.fiberIdWith((id) => - core.flatMap( + ensuring( core.flatMap( forkDaemon(core.interruptible(self)), (processing) => @@ -3751,19 +3770,18 @@ export const invokeWithInterrupt: ( }) }) ), - () => - core.suspend(() => { - const residual = entries.flatMap((entry) => { - if (!entry.state.completed) { - return [entry] - } - return [] - }) - return core.forEachSequentialDiscard( - residual, - (entry) => complete(entry.request as any, core.exitInterrupt(id)) - ) + core.suspend(() => { + const residual = entries.flatMap((entry) => { + if (!entry.state.completed) { + return [entry] + } + return [] }) + return core.forEachSequentialDiscard( + residual, + (entry) => complete(entry.request as any, core.exitInterrupt(id)) + ) + }) ) ) diff --git a/packages/effect/src/internal/logger.ts b/packages/effect/src/internal/logger.ts index a1a3bebe5c7..b416a8fa354 100644 --- a/packages/effect/src/internal/logger.ts +++ b/packages/effect/src/internal/logger.ts @@ -456,7 +456,7 @@ const prettyLoggerBrowser = (options: { console.groupCollapsed(firstLine, ...firstParams) if (!Cause.isEmpty(cause)) { - console.error(Cause.pretty(cause, { renderErrorCause: true })) + console.error(...Cause.prettyErrors(cause)) } if (messageIndex < message.length) { diff --git a/packages/effect/src/internal/managedRuntime.ts b/packages/effect/src/internal/managedRuntime.ts index 38a2564e875..8addd717cde 100644 --- a/packages/effect/src/internal/managedRuntime.ts +++ b/packages/effect/src/internal/managedRuntime.ts @@ -7,6 +7,7 @@ import type * as M from "../ManagedRuntime.js" import { pipeArguments } from "../Pipeable.js" import { hasProperty } from "../Predicate.js" import type * as Runtime from "../Runtime.js" +import * as Scheduler from "../Scheduler.js" import * as Scope from "../Scope.js" import type { Mutable } from "../Types.js" import * as core from "./core.js" @@ -57,8 +58,9 @@ export const make = ( memoMap = memoMap ?? internalLayer.unsafeMakeMemoMap() const scope = internalRuntime.unsafeRunSyncEffect(fiberRuntime.scopeMake()) let buildFiber: Fiber.RuntimeFiber, ER> | undefined - const runtimeEffect = core.withFiberRuntime, ER>((fiber) => { + const runtimeEffect = core.suspend(() => { if (!buildFiber) { + const scheduler = new Scheduler.SyncScheduler() buildFiber = internalRuntime.unsafeForkEffect( core.tap( Scope.extend( @@ -69,8 +71,9 @@ export const make = ( self.cachedRuntime = rt } ), - { scope, scheduler: fiber.currentScheduler } + { scope, scheduler } ) + scheduler.flush() } return core.flatten(buildFiber.await) }) diff --git a/packages/effect/src/internal/matcher.ts b/packages/effect/src/internal/matcher.ts index 44464f5113a..3bfe589de55 100644 --- a/packages/effect/src/internal/matcher.ts +++ b/packages/effect/src/internal/matcher.ts @@ -351,8 +351,8 @@ export const discriminator = (field: D) => const f = pattern[pattern.length - 1] const values: Array

= pattern.slice(0, -1) as any const pred = values.length === 1 - ? (_: any) => _[field] === values[0] - : (_: any) => values.includes(_[field]) + ? (_: any) => _ != null && _[field] === values[0] + : (_: any) => _ != null && values.includes(_[field]) return ( self: Matcher @@ -377,7 +377,7 @@ export const discriminatorStartsWith = (field: D) => pattern: P, f: Fn ) => { - const pred = (_: any) => typeof _[field] === "string" && _[field].startsWith(pattern) + const pred = (_: any) => _ != null && typeof _[field] === "string" && _[field].startsWith(pattern) return ( self: Matcher diff --git a/packages/effect/src/internal/rcMap.ts b/packages/effect/src/internal/rcMap.ts index 12c84ef44f2..b5980220b3a 100644 --- a/packages/effect/src/internal/rcMap.ts +++ b/packages/effect/src/internal/rcMap.ts @@ -4,7 +4,7 @@ import type * as Deferred from "../Deferred.js" import * as Duration from "../Duration.js" import type { Effect } from "../Effect.js" import type { RuntimeFiber } from "../Fiber.js" -import { dual, identity } from "../Function.js" +import { constant, dual, flow, identity } from "../Function.js" import * as MutableHashMap from "../MutableHashMap.js" import { pipeArguments } from "../Pipeable.js" import type * as RcMap from "../RcMap.js" @@ -33,6 +33,7 @@ declare namespace State { readonly deferred: Deferred.Deferred readonly scope: Scope.CloseableScope readonly finalizer: Effect + readonly idleTimeToLive: Duration.Duration fiber: RuntimeFiber | undefined expiresAt: number refCount: number @@ -58,7 +59,7 @@ class RcMapImpl implements RcMap.RcMap { readonly lookup: (key: K) => Effect, readonly context: Context.Context, readonly scope: Scope.Scope, - readonly idleTimeToLive: Duration.Duration | undefined, + readonly idleTimeToLive: ((key: K) => Duration.Duration) | undefined, readonly capacity: number ) { this[TypeId] = variance @@ -73,27 +74,32 @@ class RcMapImpl implements RcMap.RcMap { export const make: { (options: { readonly lookup: (key: K) => Effect - readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined readonly capacity?: undefined }): Effect, never, Scope.Scope | R> (options: { readonly lookup: (key: K) => Effect - readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined readonly capacity: number }): Effect, never, Scope.Scope | R> } = (options: { readonly lookup: (key: K) => Effect - readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined readonly capacity?: number | undefined }) => core.withFiberRuntime, never, R | Scope.Scope>((fiber) => { const context = fiber.getFiberRef(core.currentContext) as Context.Context const scope = Context.get(context, fiberRuntime.scopeTag) + const idleTimeToLive = options.idleTimeToLive === undefined + ? undefined + : typeof options.idleTimeToLive === "function" + ? flow(options.idleTimeToLive, Duration.decode) + : constant(Duration.decode(options.idleTimeToLive)) const self = new RcMapImpl( options.lookup as any, context, scope, - options.idleTimeToLive ? Duration.decode(options.idleTimeToLive) : undefined, + idleTimeToLive, Math.max(options.capacity ?? Number.POSITIVE_INFINITY, 0) ) return core.as( @@ -169,10 +175,12 @@ const acquire = core.fnUntraced(function*(self: RcMapImpl, key core.flatMap((exit) => core.deferredDone(deferred, exit)), circular.forkIn(scope) ) + const idleTimeToLive = self.idleTimeToLive ? self.idleTimeToLive(key) : Duration.zero const entry: State.Entry = { deferred, scope, finalizer: undefined as any, + idleTimeToLive, fiber: undefined, expiresAt: 0, refCount: 1 @@ -192,7 +200,7 @@ const release = (self: RcMapImpl, key: K, entry: State.Entry(self: RcMapImpl, key: K, entry: State.Entry { @@ -276,10 +284,12 @@ export const touch: { (self_: RcMap.RcMap, key: K) => coreEffect.clockWith((clock) => { const self = self_ as RcMapImpl - if (!self.idleTimeToLive || self.state._tag === "Closed") return core.void + if (self.state._tag === "Closed") return core.void const o = MutableHashMap.get(self.state.map, key) if (o._tag === "None") return core.void - o.value.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(self.idleTimeToLive) + const entry = o.value + if (Duration.isZero(entry.idleTimeToLive)) return core.void + entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(entry.idleTimeToLive) return core.void }) ) diff --git a/packages/effect/src/internal/stream.ts b/packages/effect/src/internal/stream.ts index ada16cbe5a6..8c2d46d3d8b 100644 --- a/packages/effect/src/internal/stream.ts +++ b/packages/effect/src/internal/stream.ts @@ -1397,6 +1397,7 @@ export const changesWith = dual< return [Option.some(output), pipe(outputs, Chunk.append(output))] as const } ) + if (Chunk.isEmpty(newChunk)) return writer(newLast) return core.flatMap( core.write(newChunk), () => writer(newLast) @@ -8773,7 +8774,7 @@ export const decodeText = dual< >((args) => isStream(args[0]), (self, encoding = "utf-8") => suspend(() => { const decoder = new TextDecoder(encoding) - return map(self, (s) => decoder.decode(s)) + return map(self, (s) => decoder.decode(s, { stream: true })) })) /** @internal */ diff --git a/packages/effect/src/internal/version.ts b/packages/effect/src/internal/version.ts index b5f2015b7dd..27afe5af255 100644 --- a/packages/effect/src/internal/version.ts +++ b/packages/effect/src/internal/version.ts @@ -1,4 +1,4 @@ -let moduleVersion = "3.19.15" +let moduleVersion = "3.21.2" export const getCurrentVersion = () => moduleVersion diff --git a/packages/effect/test/Cause.test.ts b/packages/effect/test/Cause.test.ts index e3f5bac80c6..0220598384a 100644 --- a/packages/effect/test/Cause.test.ts +++ b/packages/effect/test/Cause.test.ts @@ -926,16 +926,16 @@ describe("Cause", () => { describe("Formatting", () => { it("prettyErrors", () => { deepStrictEqual(Cause.prettyErrors(empty), []) - deepStrictEqual(Cause.prettyErrors(failure), [new internal.PrettyError("error")]) - deepStrictEqual(Cause.prettyErrors(defect), [new internal.PrettyError("defect")]) + deepStrictEqual(Cause.prettyErrors(failure), [internal.makePrettyError("error")]) + deepStrictEqual(Cause.prettyErrors(defect), [internal.makePrettyError("defect")]) deepStrictEqual(Cause.prettyErrors(interruption), []) deepStrictEqual(Cause.prettyErrors(sequential), [ - new internal.PrettyError("error"), - new internal.PrettyError("defect") + internal.makePrettyError("error"), + internal.makePrettyError("defect") ]) deepStrictEqual(Cause.prettyErrors(parallel), [ - new internal.PrettyError("error"), - new internal.PrettyError("defect") + internal.makePrettyError("error"), + internal.makePrettyError("defect") ]) }) diff --git a/packages/effect/test/Cron.test.ts b/packages/effect/test/Cron.test.ts index 0452ea792b3..8448aa6cb2b 100644 --- a/packages/effect/test/Cron.test.ts +++ b/packages/effect/test/Cron.test.ts @@ -8,6 +8,9 @@ const match = (input: Cron.Cron | string, date: DateTime.DateTime.Input) => const next = (input: Cron.Cron | string, after?: DateTime.DateTime.Input) => Cron.next(Cron.isCron(input) ? input : Cron.unsafeParse(input), after) +const prev = (input: Cron.Cron | string, after?: DateTime.DateTime.Input) => + Cron.prev(Cron.isCron(input) ? input : Cron.unsafeParse(input), after) + describe("Cron", () => { it("parse", () => { // At 04:00 on every day-of-month from 8 through 14. @@ -126,6 +129,136 @@ describe("Cron", () => { deepStrictEqual(next(Cron.unsafeParse("5 0 8 2 *", london), after), DateTime.toDateUtc(amsterdamTime)) }) + it("prev", () => { + const utc = DateTime.zoneUnsafeMakeNamed("UTC") + const before = new Date("2024-01-04T16:21:00Z") + deepStrictEqual(prev(Cron.unsafeParse("5 0 8 2 *", utc), before), new Date("2023-02-08T00:05:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("15 14 1 * *", utc), before), new Date("2024-01-01T14:15:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("23 0-20/2 * * * 0", utc), before), new Date("2023-12-31T23:20:23.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("5 4 * * SUN", utc), before), new Date("2023-12-31T04:05:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("5 4 * DEC SUN", utc), before), new Date("2023-12-31T04:05:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("30 5 0 8 2 *", utc), before), new Date("2023-02-08T00:05:30.000Z")) + + const wednesday = new Date("2025-10-22T01:00:00.000Z") + deepStrictEqual(prev(Cron.unsafeParse("0 1 * * MON", utc), wednesday), new Date("2025-10-20T01:00:00.000Z")) + deepStrictEqual(next(Cron.unsafeParse("0 1 * * MON", utc), wednesday), new Date("2025-10-27T01:00:00.000Z")) + deepStrictEqual(prev(Cron.unsafeParse("0 1 * * TUE", utc), wednesday), new Date("2025-10-21T01:00:00.000Z")) + deepStrictEqual(next(Cron.unsafeParse("0 1 * * TUE", utc), wednesday), new Date("2025-10-28T01:00:00.000Z")) + }) + + it("returns the latest second when rolling back a minute", () => { + const utc = DateTime.zoneUnsafeMakeNamed("UTC") + const expr = Cron.unsafeParse("10,30 * * * * *", utc) + const before = new Date("2024-01-01T00:00:05.000Z") + deepStrictEqual(prev(expr, before), new Date("2023-12-31T23:59:30.000Z")) + }) + + it("forward and reverse sequences stay aligned", () => { + const cases = [ + ["5 2 * * 1", "2020-01-01T00:00:01Z", "2021-01-01T00:00:01Z"], + ["0 12 1 * *", "2020-01-01T00:00:01Z", "2021-01-01T00:00:01Z"], + ["10,30 * * * * *", "2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z"] + ] as const + + const gather = ( + generator: IterableIterator, + lower: Date, + upper: Date, + direction: "forward" | "reverse" + ) => { + const res: Array = [] + for (const date of generator) { + if (direction === "forward" ? date >= upper : date <= lower) { + break + } + res.push(date) + } + return res + } + + for (const [expr, lowerStr, upperStr] of cases) { + const lower = new Date(lowerStr) + const upper = new Date(upperStr) + const cron = Cron.unsafeParse(expr, DateTime.zoneUnsafeMakeNamed("UTC")) + + const forward = gather(Cron.sequence(cron, lower), lower, upper, "forward") + const reverse = gather(Cron.sequenceReverse(cron, upper), lower, upper, "reverse").reverse() + + deepStrictEqual(forward, reverse) + } + }) + + it("prev prefers the latest matching day within the previous month", () => { + const cron = Cron.unsafeParse("0 0 8 5,20 * *", DateTime.zoneUnsafeMakeNamed("UTC")) + const before = new Date("2024-06-03T00:00:00.000Z") + deepStrictEqual(prev(cron, before), new Date("2024-05-20T08:00:00.000Z")) + }) + + it("prev wraps weekday using the last allowed value", () => { + const cron = Cron.unsafeParse("0 1 * * MON,FRI", DateTime.zoneUnsafeMakeNamed("UTC")) + const sunday = new Date("2025-10-19T12:00:00.000Z") // Sunday + deepStrictEqual(prev(cron, sunday), new Date("2025-10-17T01:00:00.000Z")) // Friday + }) + + it("prev chooses the later occurrence in DST fall-back", () => { + const make = (s: string) => DateTime.makeZonedFromString(s).pipe(Option.getOrThrow) + const tz = "Europe/Berlin" + const cron = Cron.unsafeParse("0 30 2 * * *", tz) + const before = make("2024-10-27T03:30:00.000+01:00[Europe/Berlin]") + const result = DateTime.unsafeMakeZoned(prev(cron, before), { timeZone: tz }) + deepStrictEqual(result.pipe(DateTime.formatIsoZoned), "2024-10-27T02:30:00.000+02:00[Europe/Berlin]") + }) + + it("prev respects combined day-of-month and weekday constraints", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 9 1,15 * MON", tz) + const before = new Date("2024-04-02T12:00:00.000Z") // Tue after a matching Monday the 1st + deepStrictEqual(prev(cron, before), new Date("2024-04-01T09:00:00.000Z")) + }) + + it("prev handles step expressions across day boundary", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 */7 8-10 * * *", tz) + const before = new Date("2024-01-01T08:01:00.000Z") + deepStrictEqual(prev(cron, before), new Date("2024-01-01T08:00:00.000Z")) + }) + + it("prev works with fixed offset time zones", () => { + const offset = DateTime.zoneMakeOffset(2 * 60 * 60 * 1000) // UTC+2 + const cron = Cron.unsafeParse("0 0 10 * * *", offset) + const before = new Date("2024-05-01T07:00:00.000Z") // before 10:00 local (08:00Z) + deepStrictEqual(prev(cron, before), new Date("2024-04-30T08:00:00.000Z")) + }) + + it("prev wraps across year boundary", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 1 1 *", tz) + const from = new Date("2024-01-01T00:00:00.000Z") + deepStrictEqual(prev(cron, from), new Date("2023-01-01T00:00:00.000Z")) + }) + + it("prev handles day 31 skipping months without it", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 31 * *", tz) + const from = new Date("2024-03-01T00:00:00.000Z") + // Should skip Feb (no day 31) and go to Jan 31 + deepStrictEqual(prev(cron, from), new Date("2024-01-31T00:00:00.000Z")) + }) + + it("prev clamps to the last valid day when rolling back a month with only month constraints", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 0 * FEB *", tz) + const from = new Date("2024-03-31T12:00:00.000Z") + deepStrictEqual(prev(cron, from), new Date("2024-02-29T00:00:00.000Z")) + }) + + it("prev with multiple months specified", () => { + const tz = DateTime.zoneUnsafeMakeNamed("UTC") + const cron = Cron.unsafeParse("0 0 15 1,4,7,10 *", tz) // Quarterly on 15th + const from = new Date("2024-05-01T00:00:00.000Z") + deepStrictEqual(prev(cron, from), new Date("2024-04-15T00:00:00.000Z")) + }) + it("sequence", () => { const start = new Date("2024-01-01 00:00:00") const generator = Cron.sequence(Cron.unsafeParse("23 0-20/2 * * 0"), start) @@ -136,6 +269,17 @@ describe("Cron", () => { deepStrictEqual(generator.next().value, new Date("2024-01-07 08:23:00")) }) + it("sequenceReverse", () => { + const start = new Date("2024-01-01 00:00:00Z") + const utc = DateTime.zoneUnsafeMakeNamed("UTC") + const generator = Cron.sequenceReverse(Cron.unsafeParse("23 0-20/2 * * 0", utc), start) + deepStrictEqual(generator.next().value, new Date("2023-12-31 20:23:00Z")) + deepStrictEqual(generator.next().value, new Date("2023-12-31 18:23:00Z")) + deepStrictEqual(generator.next().value, new Date("2023-12-31 16:23:00Z")) + deepStrictEqual(generator.next().value, new Date("2023-12-31 14:23:00Z")) + deepStrictEqual(generator.next().value, new Date("2023-12-31 12:23:00Z")) + }) + it("equal", () => { const cron = Cron.unsafeParse("23 0-20/2 * * 0") assertTrue(Equal.equals(cron, cron)) diff --git a/packages/effect/test/Effect/query-defect.test.ts b/packages/effect/test/Effect/query-defect.test.ts new file mode 100644 index 00000000000..f7d97bec2b7 --- /dev/null +++ b/packages/effect/test/Effect/query-defect.test.ts @@ -0,0 +1,61 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as Request from "effect/Request" +import * as RequestResolver from "effect/RequestResolver" + +class GetValue extends Request.TaggedClass("GetValue") {} + +describe("batched resolver defect", () => { + // When a batched resolver dies with a defect, the request Deferreds are + // never completed and consumers hang forever. The cleanup that completes + // uncompleted entries only runs on success (flatMap/OP_ON_SUCCESS) but + // should run on all exits. + it.live("resolver defect should not hang consumers", () => + Effect.gen(function*() { + const resolver = RequestResolver.makeBatched((_requests: Array) => Effect.die("boom")) + + const fiber = yield* Effect.request(new GetValue({ id: 1 }), resolver).pipe( + Effect.fork + ) + + // Wait briefly then check if the fiber completed. + // If the bug is present, the fiber hangs on deferredAwait forever. + yield* Effect.sleep("500 millis") + const poll = yield* Fiber.poll(fiber) + + assertTrue( + poll._tag === "Some", + "Fiber should have completed — resolver defect must not leave consumers hanging" + ) + + if (poll._tag === "Some") { + assertTrue(Exit.isFailure(poll.value)) + } + })) + + it.live("resolver defect should not hang multiple consumers", () => + Effect.gen(function*() { + const resolver = RequestResolver.makeBatched((_requests: Array) => Effect.die("boom")) + + const fiber = yield* Effect.forEach( + [1, 2, 3], + (id) => Effect.request(new GetValue({ id }), resolver), + { batching: true, concurrency: "unbounded" } + ).pipe(Effect.fork) + + yield* Effect.sleep("500 millis") + const poll = yield* Fiber.poll(fiber) + + assertTrue( + poll._tag === "Some", + "Fiber should have completed — resolver defect must not leave consumers hanging" + ) + + if (poll._tag === "Some") { + assertTrue(Exit.isFailure(poll.value)) + } + })) +}) diff --git a/packages/effect/test/Effect/semaphore.test.ts b/packages/effect/test/Effect/semaphore.test.ts index 781a5c554c2..ff57199f601 100644 --- a/packages/effect/test/Effect/semaphore.test.ts +++ b/packages/effect/test/Effect/semaphore.test.ts @@ -2,6 +2,9 @@ import { assert, describe, it } from "@effect/vitest" import { strictEqual } from "@effect/vitest/utils" import * as D from "effect/Duration" import * as Effect from "effect/Effect" +import * as FiberId from "effect/FiberId" +import * as Option from "effect/Option" +import * as Scheduler from "effect/Scheduler" import * as TestClock from "effect/TestClock" describe("Effect", () => { @@ -56,4 +59,27 @@ describe("Effect", () => { yield* TestClock.adjust(1) assert.isTrue(fiber.unsafePoll() !== null) })) + + it.effect("take interruption does not leak permits", () => + Effect.gen(function*() { + const scheduler = new Scheduler.ControlledScheduler() + const sem = yield* Effect.makeSemaphore(0) + const waiter = yield* sem.take(1).pipe( + Effect.withScheduler(scheduler), + Effect.fork + ) + + yield* Effect.yieldNow() + yield* sem.release(1).pipe(Effect.withScheduler(scheduler)) + assert.isNull(waiter.unsafePoll()) + + scheduler.step() + assert.isNull(waiter.unsafePoll()) + + waiter.unsafeInterruptAsFork(FiberId.none) + scheduler.step() + + const result = yield* sem.withPermitsIfAvailable(1)(Effect.void) + assert.isTrue(Option.isSome(result)) + })) }) diff --git a/packages/effect/test/Equal.test.ts b/packages/effect/test/Equal.test.ts index 4098a09fc62..11fa1013021 100644 --- a/packages/effect/test/Equal.test.ts +++ b/packages/effect/test/Equal.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@effect/vitest" import { assertFalse, assertTrue } from "@effect/vitest/utils" -import { Equal } from "effect" +import { Equal, Utils } from "effect" describe("Equal", () => { it("invalid Date", () => { @@ -14,4 +14,52 @@ describe("Equal", () => { const invalid = new Date("invalid") assertFalse(Equal.equals(epoch, invalid)) }) + + describe("structuralRegion", () => { + it("null vs null", () => { + Utils.structuralRegion(() => { + assertTrue(Equal.equals(null, null)) + }) + }) + + it("null vs object", () => { + Utils.structuralRegion(() => { + assertFalse(Equal.equals(null, { a: 1 })) + }) + }) + + it("object vs null", () => { + Utils.structuralRegion(() => { + assertFalse(Equal.equals({ a: 1 }, null)) + }) + }) + + it("null vs string", () => { + Utils.structuralRegion(() => { + assertFalse(Equal.equals(null, "hello")) + }) + }) + + it("null vs array", () => { + Utils.structuralRegion(() => { + assertFalse(Equal.equals(null, [1, 2, 3])) + }) + }) + + it("nested object with null values", () => { + const a = { name: "test", address: { city: "NYC", zip: null } } + const b = { name: "test", address: { city: "NYC", zip: null } } + Utils.structuralRegion(() => { + assertTrue(Equal.equals(a, b)) + }) + }) + + it("nested object with null vs non-null", () => { + const a = { name: "test", value: null } + const b = { name: "test", value: 42 } + Utils.structuralRegion(() => { + assertFalse(Equal.equals(a, b)) + }) + }) + }) }) diff --git a/packages/effect/test/ManagedRuntime.test.ts b/packages/effect/test/ManagedRuntime.test.ts index 29549ce5f12..4b3fee526bd 100644 --- a/packages/effect/test/ManagedRuntime.test.ts +++ b/packages/effect/test/ManagedRuntime.test.ts @@ -76,4 +76,10 @@ describe("ManagedRuntime", () => { const result = Context.get(runtime.context, tag) strictEqual(result, "test") }) + + it("is built synchronously with runFork", () => { + const runtime = ManagedRuntime.make(Layer.empty) + runtime.runFork(Effect.void) + runtime.runSync(Effect.void) + }) }) diff --git a/packages/effect/test/Match.test.ts b/packages/effect/test/Match.test.ts index ea6979b1712..a631c67e5b8 100644 --- a/packages/effect/test/Match.test.ts +++ b/packages/effect/test/Match.test.ts @@ -207,6 +207,36 @@ describe("Match", () => { doesNotThrow(() => match(undefined)) }) + it("Match.tag with nullable union", () => { + const match = M.type<{ _tag: "A" } | undefined>().pipe( + M.tag("A", () => "matched A"), + M.when(undefined, () => "matched undefined"), + M.exhaustive + ) + strictEqual(match({ _tag: "A" }), "matched A") + strictEqual(match(undefined), "matched undefined") + }) + + it("Match.tag with null union", () => { + const match = M.type<{ _tag: "A" } | null>().pipe( + M.tag("A", () => "matched A"), + M.when(null, () => "matched null"), + M.exhaustive + ) + strictEqual(match({ _tag: "A" }), "matched A") + strictEqual(match(null), "matched null") + }) + + it("Match.tagStartsWith with nullable union", () => { + const match = M.type<{ _tag: "A.B" } | undefined>().pipe( + M.tagStartsWith("A", () => "matched A prefix"), + M.when(undefined, () => "matched undefined"), + M.exhaustive + ) + strictEqual(match({ _tag: "A.B" }), "matched A prefix") + strictEqual(match(undefined), "matched undefined") + }) + it("discriminator multiple", () => { const result = pipe( M.value(Either.right(0)), diff --git a/packages/effect/test/RcMap.test.ts b/packages/effect/test/RcMap.test.ts index e661149a5c0..5d5ec2b1fb3 100644 --- a/packages/effect/test/RcMap.test.ts +++ b/packages/effect/test/RcMap.test.ts @@ -175,4 +175,63 @@ describe("RcMap", () => { deepStrictEqual(yield* RcMap.keys(map), ["foo", "bar", "baz"]) })) + + it.scoped("dynamic idleTimeToLive", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ), + idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000 + }) + + deepStrictEqual(acquired, []) + + yield* Effect.scoped(RcMap.get(map, "short:a")) + yield* Effect.scoped(RcMap.get(map, "long:b")) + deepStrictEqual(acquired, ["short:a", "long:b"]) + deepStrictEqual(released, []) + + yield* TestClock.adjust(500) + deepStrictEqual(released, ["short:a"]) + + yield* TestClock.adjust(1500) + deepStrictEqual(released, ["short:a", "long:b"]) + })) + + it.scoped("dynamic idleTimeToLive with touch", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ), + idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000 + }) + + yield* Effect.scoped(RcMap.get(map, "short:a")) + deepStrictEqual(acquired, ["short:a"]) + deepStrictEqual(released, []) + + yield* TestClock.adjust(250) + yield* RcMap.touch(map, "short:a") + yield* TestClock.adjust(250) + deepStrictEqual(released, []) + + yield* TestClock.adjust(250) + deepStrictEqual(released, ["short:a"]) + })) }) diff --git a/packages/effect/test/Runtime.test.ts b/packages/effect/test/Runtime.test.ts index 6d124b33eff..196a5ce5bb7 100644 --- a/packages/effect/test/Runtime.test.ts +++ b/packages/effect/test/Runtime.test.ts @@ -1,3 +1,5 @@ +import { AsyncLocalStorage } from "node:async_hooks" + import { describe, it } from "@effect/vitest" import { assertTrue, deepStrictEqual, strictEqual, throwsAsync } from "@effect/vitest/utils" import { Effect, Exit, FiberRef, Layer, pipe, Runtime } from "effect" @@ -68,6 +70,61 @@ describe("Runtime", () => { ) }) + it.effect("runPromise isolates AsyncLocalStorage across concurrent calls", () => + Effect.gen(function*() { + type RequestStore = { + readonly userId: string + } + + type Result = { + readonly expected: string + readonly observed: string + } + + const requestStore = new AsyncLocalStorage() + + const readAlsOnScheduledContinuation = Effect.withFiberRuntime((fiber) => + Effect.sync(() => requestStore.getStore()?.userId ?? "NONE").pipe( + Effect.flatMap((expected) => + Effect.async((resume) => { + fiber.currentScheduler.scheduleTask( + () => { + resume( + Effect.succeed({ + expected, + observed: requestStore.getStore()?.userId ?? "NONE" + }) + ) + }, + 0, + fiber + ) + }) + ) + ) + ) + + const runtime = yield* Effect.runtime() + + const runRequest = (userId: string): Promise => + requestStore.run( + { userId }, + () => Runtime.runPromise(runtime)(readAlsOnScheduledContinuation) + ) + + const results = yield* Effect.promise(() => + Promise.all([ + runRequest("user-A"), + runRequest("user-B") + ]) + ) + + deepStrictEqual(results, [ + { expected: "user-A", observed: "user-A" }, + { expected: "user-B", observed: "user-B" } + ]) + })) + it("runPromiseExit", async () => { deepStrictEqual( await Runtime.runPromiseExit(Runtime.defaultRuntime)(Effect.promise(async () => 1)), diff --git a/packages/effect/test/Schema/Schema/Struct/omit.test.ts b/packages/effect/test/Schema/Schema/Struct/omit.test.ts index 47059890948..da5f2b9c822 100644 --- a/packages/effect/test/Schema/Schema/Struct/omit.test.ts +++ b/packages/effect/test/Schema/Schema/Struct/omit.test.ts @@ -7,4 +7,16 @@ describe("omit", () => { const schema = S.Struct({ a: S.String, b: S.Number, c: S.Boolean }).omit("c") deepStrictEqual(schema.fields, { a: S.String, b: S.Number }) }) + + it("should preserve index signatures on Struct with optionalWith default", () => { + const schema = S.Struct( + { a: S.String, b: S.optionalWith(S.Number, { default: () => 0 }) }, + S.Record({ key: S.String, value: S.Boolean }) + ) + const plain = S.Struct( + { a: S.String, b: S.Number }, + S.Record({ key: S.String, value: S.Boolean }) + ) + deepStrictEqual(schema.pipe(S.omit("a")).ast, plain.pipe(S.omit("a")).ast) + }) }) diff --git a/packages/effect/test/Schema/Schema/Tuple/Tuple.test.ts b/packages/effect/test/Schema/Schema/Tuple/Tuple.test.ts index b90e1cfe526..bc5a5f489bb 100644 --- a/packages/effect/test/Schema/Schema/Tuple/Tuple.test.ts +++ b/packages/effect/test/Schema/Schema/Tuple/Tuple.test.ts @@ -323,6 +323,21 @@ describe("Tuple", () => { └─ is missing` ) }) + + it("[String] + [Boolean, String, Number, Number] validates every post-rest index", async () => { + const schema = S.Tuple([S.String], S.Boolean, S.String, S.NumberFromString, S.NumberFromString) + + await Util.assertions.decoding.fail( + schema, + ["a", true, "b", "1", "x"], + `readonly [string, ...boolean[], string, NumberFromString, NumberFromString] +└─ [4] + └─ NumberFromString + └─ Transformation process failure + └─ Unable to decode "x" into a number` + ) + await Util.assertions.decoding.succeed(schema, ["a", true, "b", "1", "2"], ["a", true, "b", 1, 2]) + }) }) describe("encoding", () => { diff --git a/packages/effect/test/Schema/SchemaAST/getPropertySignatures.test.ts b/packages/effect/test/Schema/SchemaAST/getPropertySignatures.test.ts index 173ee4fb6ed..8e659fb7368 100644 --- a/packages/effect/test/Schema/SchemaAST/getPropertySignatures.test.ts +++ b/packages/effect/test/Schema/SchemaAST/getPropertySignatures.test.ts @@ -43,4 +43,30 @@ describe("getPropertySignatures", () => { new AST.PropertySignature("b", S.Number.ast, false, true) ]) }) + + it("Transformation (Struct with optionalWith default)", () => { + const schema = S.Struct({ + a: S.String, + b: S.optionalWith(S.Number, { default: () => 0 }) + }) + deepStrictEqual(AST.getPropertySignatures(schema.ast), [ + new AST.PropertySignature("a", S.String.ast, false, true), + new AST.PropertySignature("b", S.Number.ast, false, true) + ]) + }) + + it("Transformation (Struct with optionalWith as Option)", () => { + const schema = S.Struct({ + a: S.String, + b: S.optionalWith(S.Number, { as: "Option" }) + }) + const signatures = AST.getPropertySignatures(schema.ast) + deepStrictEqual(signatures.length, 2) + deepStrictEqual(signatures[0], new AST.PropertySignature("a", S.String.ast, false, true)) + deepStrictEqual(signatures[1].name, "b") + deepStrictEqual(signatures[1].isOptional, false) + deepStrictEqual(signatures[1].isReadonly, true) + // b's type on the decoded side is Option (a Declaration AST) + deepStrictEqual(signatures[1].type._tag, "Declaration") + }) }) diff --git a/packages/effect/test/Stream/encoding.test.ts b/packages/effect/test/Stream/encoding.test.ts index 111686bef90..46aa43c5f89 100644 --- a/packages/effect/test/Stream/encoding.test.ts +++ b/packages/effect/test/Stream/encoding.test.ts @@ -22,4 +22,45 @@ describe("Stream", () => { ) deepStrictEqual(Chunk.toReadonlyArray(decoded), items) })) + + it.effect("decodeText handles multi-byte characters split across chunks", () => + Effect.gen(function*() { + // 🌍 is U+1F30D — four UTF-8 bytes: [0xF0, 0x9F, 0x8C, 0x8D] + const bytes = new TextEncoder().encode("🌍") + + // Split the bytes mid-character across two chunks + const stream = Stream.fromChunks( + Chunk.of(bytes.slice(0, 2)), // [0xF0, 0x9F] + Chunk.of(bytes.slice(2, 4)) // [0x8C, 0x8D] + ) + + const result = yield* pipe( + stream, + Stream.decodeText(), + Stream.mkString + ) + + strictEqual(result, "🌍") + })) + + it.effect("decodeText handles mixed ASCII and multi-byte characters across chunks", () => + Effect.gen(function*() { + // "Hello 🌍!" encoded as UTF-8 + const bytes = new TextEncoder().encode("Hello 🌍!") + + // Split in the middle of the emoji (after "Hello " + first 2 bytes of emoji) + const splitPoint = 6 + 2 // "Hello " is 6 bytes, then 2 of 4 emoji bytes + const stream = Stream.fromChunks( + Chunk.of(bytes.slice(0, splitPoint)), + Chunk.of(bytes.slice(splitPoint)) + ) + + const result = yield* pipe( + stream, + Stream.decodeText(), + Stream.mkString + ) + + strictEqual(result, "Hello 🌍!") + })) }) diff --git a/packages/effect/test/TestClock.test.ts b/packages/effect/test/TestClock.test.ts index 597e182dc8f..14356bef74e 100644 --- a/packages/effect/test/TestClock.test.ts +++ b/packages/effect/test/TestClock.test.ts @@ -1,5 +1,5 @@ import { describe, it } from "@effect/vitest" -import { deepStrictEqual } from "@effect/vitest/utils" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { DateTime, Effect, TestClock } from "effect" describe("TestClock", () => { @@ -23,5 +23,12 @@ describe("TestClock", () => { const now = yield* DateTime.now deepStrictEqual(now, arbitraryDateTime) })) + + it.effect("should floor nanoseconds for fractional millisecond instants", () => + Effect.gen(function*() { + yield* TestClock.setTime(199023438.0000004) + const testClock = yield* TestClock.testClock() + strictEqual(testClock.unsafeCurrentTimeNanos(), 199023438000000n) + })) }) }) diff --git a/packages/experimental/CHANGELOG.md b/packages/experimental/CHANGELOG.md index 3927282fddf..d48c7f6112b 100644 --- a/packages/experimental/CHANGELOG.md +++ b/packages/experimental/CHANGELOG.md @@ -1,5 +1,21 @@ # @effect/experimental +## 0.60.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + +## 0.59.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + ## 0.58.0 ### Patch Changes diff --git a/packages/experimental/package.json b/packages/experimental/package.json index ff08f8da002..dc5be64baf2 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@effect/experimental", - "version": "0.58.0", + "version": "0.60.0", "type": "module", "license": "MIT", "description": "Experimental modules for the Effect ecosystem", diff --git a/packages/opentelemetry/CHANGELOG.md b/packages/opentelemetry/CHANGELOG.md index 62b760a863d..74656668b1e 100644 --- a/packages/opentelemetry/CHANGELOG.md +++ b/packages/opentelemetry/CHANGELOG.md @@ -1,5 +1,34 @@ # @effect/opentelemetry +## 0.63.0 + +### Patch Changes + +- [#5780](https://github.com/Effect-TS/effect/pull/5780) [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb) Thanks @mikearnaldi! - Add logs to first propagated span, in the following case before this fix the log would not be added to the `p` span because `Effect.fn` adds a fake span for the purpose of adding a stack frame. + + ```ts + import { Effect } from "effect" + + const f = Effect.fn(function* () { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + ``` + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + +## 0.62.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + ## 0.61.0 ### Minor Changes diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index d2092d98749..4278377d3df 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@effect/opentelemetry", - "version": "0.61.0", + "version": "0.63.0", "type": "module", "license": "MIT", "description": "OpenTelemetry integration for Effect", diff --git a/packages/opentelemetry/test/Tracer.test.ts b/packages/opentelemetry/test/Tracer.test.ts index 5ab09edd1fe..c5bb26ebe68 100644 --- a/packages/opentelemetry/test/Tracer.test.ts +++ b/packages/opentelemetry/test/Tracer.test.ts @@ -7,17 +7,27 @@ import { assert, describe, expect, it } from "@effect/vitest" import * as OtelApi from "@opentelemetry/api" import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks" import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" +import * as Console from "effect/Console" import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" import * as Layer from "effect/Layer" import * as Runtime from "effect/Runtime" import { OtelSpan } from "../src/internal/tracer.js" -const TracingLive = NodeSdk.layer(Effect.sync(() => ({ - resource: { - serviceName: "test" - }, - spanProcessor: [new SimpleSpanProcessor(new InMemorySpanExporter())] -}))) +class Exporter extends Effect.Service()("Exporter", { + effect: Effect.sync(() => ({ exporter: new InMemorySpanExporter() })) +}) {} + +const TracingLive = Layer.unwrapEffect(Effect.gen(function*() { + const { exporter } = yield* Exporter + + return NodeSdk.layer(Effect.sync(() => ({ + resource: { + serviceName: "test" + }, + spanProcessor: [new SimpleSpanProcessor(exporter)] + }))) +})).pipe(Layer.provideMerge(Exporter.Default)) // needed to test context propagation const contextManager = new AsyncHooksContextManager() @@ -227,4 +237,59 @@ describe("Tracer", () => { OtlpTracingLive )) }) + + describe("Log Attributes", () => { + it.effect("propagates attributes with Effect.fnUntraced", () => + Effect.gen(function*() { + const f = Effect.fnUntraced(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + + it.effect("propagates attributes with Effect.fn(name)", () => + Effect.gen(function*() { + const f = Effect.fn("f")(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + + it.effect("propagates attributes with Effect.fn", () => + Effect.gen(function*() { + const f = Effect.fn(function*() { + yield* Effect.logWarning("FooBar") + return yield* Effect.fail("Oops") + }) + + const p = f().pipe(Effect.withSpan("p")) + + yield* Effect.ignore(p) + + const { exporter } = yield* Exporter + + yield* Console.log(Array.from(yield* FiberRef.get(FiberRef.currentLoggers))) + + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "FooBar")) + assert.isNotEmpty(exporter.getFinishedSpans()[0].events.filter((_) => _.name === "exception")) + }).pipe(Effect.provide(TracingLive))) + }) }) diff --git a/packages/platform-browser/CHANGELOG.md b/packages/platform-browser/CHANGELOG.md index 58d92fad068..7bf255fc4ae 100644 --- a/packages/platform-browser/CHANGELOG.md +++ b/packages/platform-browser/CHANGELOG.md @@ -1,5 +1,21 @@ # @effect/platform-browser +## 0.76.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + +## 0.75.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + ## 0.74.0 ### Patch Changes diff --git a/packages/platform-browser/package.json b/packages/platform-browser/package.json index 6d3ef1480d0..2abdfe8d7d4 100644 --- a/packages/platform-browser/package.json +++ b/packages/platform-browser/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-browser", "type": "module", - "version": "0.74.0", + "version": "0.76.0", "license": "MIT", "description": "Platform specific implementations for the browser", "homepage": "https://effect.website", diff --git a/packages/platform-bun/CHANGELOG.md b/packages/platform-bun/CHANGELOG.md index d81f33fe390..21362c5165a 100644 --- a/packages/platform-bun/CHANGELOG.md +++ b/packages/platform-bun/CHANGELOG.md @@ -1,5 +1,29 @@ # @effect/platform-bun +## 0.89.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/cluster@0.58.0 + - @effect/platform@0.96.0 + - @effect/platform-node-shared@0.59.0 + - @effect/rpc@0.75.0 + - @effect/sql@0.51.0 + +## 0.88.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/cluster@0.57.0 + - @effect/platform@0.95.0 + - @effect/platform-node-shared@0.58.0 + - @effect/rpc@0.74.0 + - @effect/sql@0.50.0 + ## 0.87.1 ### Patch Changes diff --git a/packages/platform-bun/package.json b/packages/platform-bun/package.json index d5311778321..ea6d0efc8cf 100644 --- a/packages/platform-bun/package.json +++ b/packages/platform-bun/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-bun", "type": "module", - "version": "0.87.1", + "version": "0.89.0", "license": "MIT", "description": "Platform specific implementations for the Bun runtime", "homepage": "https://effect.website", diff --git a/packages/platform-node-shared/CHANGELOG.md b/packages/platform-node-shared/CHANGELOG.md index c9ca1354a29..fb1ef2004d2 100644 --- a/packages/platform-node-shared/CHANGELOG.md +++ b/packages/platform-node-shared/CHANGELOG.md @@ -1,5 +1,27 @@ # @effect/platform-node-shared +## 0.59.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/cluster@0.58.0 + - @effect/platform@0.96.0 + - @effect/rpc@0.75.0 + - @effect/sql@0.51.0 + +## 0.58.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/cluster@0.57.0 + - @effect/platform@0.95.0 + - @effect/rpc@0.74.0 + - @effect/sql@0.50.0 + ## 0.57.1 ### Patch Changes diff --git a/packages/platform-node-shared/package.json b/packages/platform-node-shared/package.json index 82bab6085bd..5425b544e86 100644 --- a/packages/platform-node-shared/package.json +++ b/packages/platform-node-shared/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-node-shared", "type": "module", - "version": "0.57.1", + "version": "0.59.0", "license": "MIT", "description": "Unified interfaces for common platform-specific services", "homepage": "https://effect.website", diff --git a/packages/platform-node/CHANGELOG.md b/packages/platform-node/CHANGELOG.md index 1287dbae270..f89937e6451 100644 --- a/packages/platform-node/CHANGELOG.md +++ b/packages/platform-node/CHANGELOG.md @@ -1,5 +1,29 @@ # @effect/platform-node +## 0.106.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/cluster@0.58.0 + - @effect/platform@0.96.0 + - @effect/platform-node-shared@0.59.0 + - @effect/rpc@0.75.0 + - @effect/sql@0.51.0 + +## 0.105.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/cluster@0.57.0 + - @effect/platform@0.95.0 + - @effect/platform-node-shared@0.58.0 + - @effect/rpc@0.74.0 + - @effect/sql@0.50.0 + ## 0.104.1 ### Patch Changes diff --git a/packages/platform-node/package.json b/packages/platform-node/package.json index 2ae75557511..18d83a32cbb 100644 --- a/packages/platform-node/package.json +++ b/packages/platform-node/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-node", "type": "module", - "version": "0.104.1", + "version": "0.106.0", "license": "MIT", "description": "Platform specific implementations for the Node.js runtime", "homepage": "https://effect.website", diff --git a/packages/platform-node/src/NodeStream.ts b/packages/platform-node/src/NodeStream.ts index c1b4eb818eb..0f143b8cfc1 100644 --- a/packages/platform-node/src/NodeStream.ts +++ b/packages/platform-node/src/NodeStream.ts @@ -3,6 +3,7 @@ */ /** - * @category models + * @since 1.0.0 + * @category re-exports */ export * from "@effect/platform-node-shared/NodeStream" diff --git a/packages/platform-node/test/HttpServer.test.ts b/packages/platform-node/test/HttpServer.test.ts index fa6724f90ad..e3604f4ab60 100644 --- a/packages/platform-node/test/HttpServer.test.ts +++ b/packages/platform-node/test/HttpServer.test.ts @@ -19,6 +19,7 @@ import { import { NodeHttpServer } from "@effect/platform-node" import { assert, describe, expect, it } from "@effect/vitest" import { Deferred, Duration, Fiber, Stream } from "effect" +import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as Option from "effect/Option" import * as Schema from "effect/Schema" @@ -479,6 +480,17 @@ describe("HttpServer", () => { expect(response.status).toEqual(499) }).pipe(Effect.provide(NodeHttpServer.layerTest))) + it.effect("causeResponse uses client abort when present", () => + Effect.gen(function*() { + const parentFiberId = yield* Effect.fiberId + const cause = Cause.sequential( + Cause.interrupt(parentFiberId), + Cause.interrupt(HttpServerError.clientAbortFiberId) + ) + const [response] = yield* HttpServerError.causeResponse(cause) + expect(response.status).toEqual(499) + })) + it.scoped("multiplex", () => Effect.gen(function*() { yield* HttpMultiplex.empty.pipe( diff --git a/packages/platform-node/test/NodeClusterSocket.test.ts b/packages/platform-node/test/NodeClusterSocket.test.ts new file mode 100644 index 00000000000..8cf5976eb88 --- /dev/null +++ b/packages/platform-node/test/NodeClusterSocket.test.ts @@ -0,0 +1,122 @@ +import { + ClusterSchema, + Entity, + MessageStorage, + RunnerAddress, + RunnerHealth, + RunnerStorage, + ShardingConfig, + SocketRunner +} from "@effect/cluster" +import { NodeClusterSocket } from "@effect/platform-node" +import { Rpc, RpcSerialization } from "@effect/rpc" +import { describe, it } from "@effect/vitest" +import { BigDecimal, Effect, Layer, Logger, LogLevel, Option, PrimaryKey, Schema } from "effect" + +class TestPayload extends Schema.Class("TestPayload")({ + id: Schema.String, + amount: Schema.BigDecimal +}) { + [PrimaryKey.symbol]() { + return this.id + } +} + +const TestEntity = Entity + .make("TestEntity", [ + Rpc.make("Process", { + payload: TestPayload, + success: Schema.Void + }) + ]) + .annotateRpcs(ClusterSchema.Persisted, true) + .annotateRpcs(ClusterSchema.Uninterruptible, true) + +const TestEntityLayer = TestEntity.toLayer( + Effect.succeed({ + Process: () => Effect.void + }) +) + +const RUNNER_PORT = 50_123 +// Build shared storage instances once, so runner and client see the same state. +// MessageStorage.layerMemory requires ShardingConfig, so we provide a minimal one. +const SharedStorage = Layer.mergeAll( + RunnerStorage.layerMemory, + MessageStorage.layerMemory +).pipe( + Layer.provide(ShardingConfig.layerDefaults) +) + +const makeRunnerLayer = (port: number) => + TestEntityLayer.pipe( + Layer.provideMerge(SocketRunner.layer), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(NodeClusterSocket.layerSocketServer), + Layer.provide(NodeClusterSocket.layerClientProtocol), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", port)), + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 + })), + Layer.provide(RpcSerialization.layerMsgPack) + ) + +const makeClientLayer = (port: number) => + SocketRunner.layerClientOnly.pipe( + Layer.provide(NodeClusterSocket.layerClientProtocol), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", port)), + runnerListenAddress: Option.some(RunnerAddress.make("localhost", port)), + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 + })), + Layer.provide(RpcSerialization.layerMsgPack) + ) + +// BigDecimal.normalize creates a circular `normalized` self-reference. +// When a persisted message is sent with discard: true, the notify path in Runners.makeRpc +// passes the raw envelope (with circular BigDecimal payload) to the runner via msgpack, +// causing RangeError: Maximum call stack size exceeded. +describe("SocketRunner", () => { + it.scopedLive( + "entity call with BigDecimal and discard should not stack overflow", + () => + Effect.gen(function*() { + // Start the runner (with socket server and entity handler) + yield* Layer.launch(makeRunnerLayer(RUNNER_PORT)).pipe(Effect.forkScoped) + + // Give the runner time to start and acquire shards + yield* Effect.sleep("2 seconds") + yield* Effect.log("Before starting the client") + + // Send a message from the client with discard: true. + // The BigDecimal is normalized to trigger the circular `normalized` self-reference. + yield* Effect.gen(function*() { + yield* Effect.log("Starting the client") + yield* Effect.sleep("2 seconds") + const makeClient = yield* TestEntity.client + // Give the client time to discover the runner + yield* Effect.sleep("3 seconds") + const client = makeClient("entity-1") + + const amount = BigDecimal.unsafeFromString("123.45") + + yield* client.Process( + TestPayload.make({ id: "req-1", amount }), + { discard: true } + ) + }).pipe( + Effect.provide(makeClientLayer(RUNNER_PORT)), + Effect.scoped + ) + }).pipe(Effect.provide( + SharedStorage.pipe(Layer.provideMerge( + Logger.minimumLogLevel(LogLevel.None) + )) + )), + 30_000 + ) +}) diff --git a/packages/platform-node/test/RpcServer.test.ts b/packages/platform-node/test/RpcServer.test.ts index 657bc77f55d..374454c0d50 100644 --- a/packages/platform-node/test/RpcServer.test.ts +++ b/packages/platform-node/test/RpcServer.test.ts @@ -2,9 +2,9 @@ import { HttpClient, HttpClientRequest, HttpRouter, HttpServer, SocketServer } f import { NodeHttpServer, NodeSocket, NodeSocketServer, NodeWorker } from "@effect/platform-node" import { RpcClient, RpcSerialization, RpcServer } from "@effect/rpc" import { assert, describe, it } from "@effect/vitest" -import { Effect, Layer } from "effect" +import { Cause, Effect, Layer } from "effect" import * as CP from "node:child_process" -import { RpcLive, User, UsersClient } from "./fixtures/rpc-schemas.js" +import { RpcLive, RpcLiveDisableFatalDefects, User, UsersClient } from "./fixtures/rpc-schemas.js" import { e2eSuite } from "./rpc-e2e.js" describe("RpcServer", () => { @@ -148,4 +148,38 @@ describe("RpcServer", () => { assert.deepStrictEqual(user, new User({ id: "1", name: "Logged in user" })) }).pipe(Effect.provide(UsersClient.layerTest))) }) + + describe("custom defect schema", () => { + const CustomDefectServer = HttpRouter.Default.serve().pipe( + Layer.provide(RpcLiveDisableFatalDefects), + Layer.provideMerge(RpcServer.layerProtocolHttp({ path: "/rpc" })) + ) + const CustomDefectClient = UsersClient.layer.pipe( + Layer.provide( + RpcClient.layerProtocolHttp({ + url: "", + transformClient: HttpClient.mapRequest(HttpClientRequest.appendUrl("/rpc")) + }) + ) + ) + const CustomDefectLayer = CustomDefectClient.pipe( + Layer.provideMerge(CustomDefectServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson]) + ) + + it.effect("preserves full defect with Schema.Unknown", () => + Effect.gen(function*() { + const client = yield* UsersClient + const cause = yield* client.ProduceDefectCustom().pipe( + Effect.sandbox, + Effect.flip + ) + const defect = Cause.squash(cause) + assert.deepStrictEqual(defect, { + message: "detailed error", + stack: "Error: detailed error\n at handler.ts:1", + code: 42 + }) + }).pipe(Effect.provide(CustomDefectLayer))) + }) }) diff --git a/packages/platform-node/test/fixtures/rpc-schemas.ts b/packages/platform-node/test/fixtures/rpc-schemas.ts index 14e8964110a..150cdad6b3a 100644 --- a/packages/platform-node/test/fixtures/rpc-schemas.ts +++ b/packages/platform-node/test/fixtures/rpc-schemas.ts @@ -58,6 +58,10 @@ export const UserRpcs = RpcGroup.make( success: Schema.Number }), Rpc.make("ProduceDefect"), + Rpc.make("ProduceErrorDefect"), + Rpc.make("ProduceDefectCustom", { + defect: Schema.Unknown + }), Rpc.make("Never"), Rpc.make("nested.test"), Rpc.make("TimedMethod", { @@ -132,6 +136,9 @@ const UsersLive = UserRpcs.toLayer(Effect.gen(function*() { GetInterrupts: () => Effect.sync(() => interrupts), GetEmits: () => Effect.sync(() => emits), ProduceDefect: () => Effect.die("boom"), + ProduceErrorDefect: () => Effect.die(new Error("error defect message")), + ProduceDefectCustom: () => + Effect.die({ message: "detailed error", stack: "Error: detailed error\n at handler.ts:1", code: 42 }), Never: () => Effect.never.pipe(Effect.onInterrupt(() => Effect.sync(() => interrupts++))), "nested.test": () => Effect.void, TimedMethod: (_) => _.shouldFail ? Effect.die("boom") : Effect.succeed(1), @@ -152,6 +159,14 @@ export const RpcLive = RpcServer.layer(UserRpcs).pipe( ]) ) +export const RpcLiveDisableFatalDefects = RpcServer.layer(UserRpcs, { disableFatalDefects: true }).pipe( + Layer.provide([ + UsersLive, + AuthLive, + TimingLive + ]) +) + const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ request }) => Effect.succeed({ ...request, diff --git a/packages/platform-node/test/rpc-e2e.ts b/packages/platform-node/test/rpc-e2e.ts index df25cc805d6..ee9ba387f5e 100644 --- a/packages/platform-node/test/rpc-e2e.ts +++ b/packages/platform-node/test/rpc-e2e.ts @@ -85,6 +85,22 @@ export const e2eSuite = ( Effect.provide(layer) )) + it.effect("defect serializes Error objects", () => + Effect.gen(function*() { + const client = yield* UsersClient + const cause = yield* client.ProduceErrorDefect().pipe( + Effect.sandbox, + Effect.flip + ) + const defect = Cause.squash(cause) + // Error details should survive serialization, not be lost as {} + assert.instanceOf(defect, Error) + assert.include(defect.message, "error defect message") + }).pipe( + RpcClient.withHeaders({ userId: "123" }), + Effect.provide(layer) + )) + it.live("never", () => Effect.gen(function*() { const client = yield* UsersClient diff --git a/packages/platform/CHANGELOG.md b/packages/platform/CHANGELOG.md index 75e9804b195..3a8ddadf231 100644 --- a/packages/platform/CHANGELOG.md +++ b/packages/platform/CHANGELOG.md @@ -1,5 +1,78 @@ # @effect/platform +## 0.96.1 + +### Patch Changes + +- [#6147](https://github.com/Effect-TS/effect/pull/6147) [`518d0e3`](https://github.com/Effect-TS/effect/commit/518d0e3f4879be6d9d9a7fa137a1820604bb3ea7) Thanks @syhstanley! - Fix `HttpLayerRouter.addHttpApi` silently skipping API-level middleware. + +- [#6191](https://github.com/Effect-TS/effect/pull/6191) [`c016642`](https://github.com/Effect-TS/effect/commit/c0166426f80b7eb8e7f7d3aecc95dcd4fdb5cb55) Thanks @IGassmann! - Update `msgpackr` to 1.11.10 to fix silent decode failures in environments that block `new Function()` at runtime (e.g. Cloudflare Workers). The new version wraps the JIT `new Function()` call in a try/catch, falling back to the interpreted path when dynamic code evaluation is blocked. + +- Updated dependencies [[`74f3267`](https://github.com/Effect-TS/effect/commit/74f3267a6cc7ed7818c4c34cc1232f7cfc7d3339)]: + - effect@3.21.2 + +## 0.96.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + +## 0.95.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + +## 0.94.5 + +### Patch Changes + +- [#6050](https://github.com/Effect-TS/effect/pull/6050) [`d67c708`](https://github.com/Effect-TS/effect/commit/d67c7089ba8616b2d48ef7324312267a2a6f310a) Thanks @tim-smart! - Backport Effect 4 `contentType` support for `HttpBody` JSON / URL-encoded constructors and `HttpServerResponse` JSON / URL-encoded helpers. + +- Updated dependencies [[`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f)]: + - effect@3.19.17 + +## 0.94.4 + +### Patch Changes + +- [#6035](https://github.com/Effect-TS/effect/pull/6035) [`22d9d27`](https://github.com/Effect-TS/effect/commit/22d9d27bc007db86d9e4748c17324fab5f950c7d) Thanks @tim-smart! - Fix `HttpServerError.causeResponse` to prefer 499 when a client abort interrupt is present. + +## 0.94.3 + +### Patch Changes + +- [#6021](https://github.com/Effect-TS/effect/pull/6021) [`0023c19`](https://github.com/Effect-TS/effect/commit/0023c19c63c402c050d496817ba92aceea7f25b7) Thanks @codewithkenzo! - Fix `HttpClientRequest.appendUrl` to properly join URL paths. + + Previously, `appendUrl` used simple string concatenation which could produce invalid URLs: + + ```typescript + // Before (broken): + appendUrl("https://api.example.com/v1", "users") + // Result: "https://api.example.com/v1users" (missing slash!) + ``` + + Now it ensures proper path joining: + + ```typescript + // After (fixed): + appendUrl("https://api.example.com/v1", "users") + // Result: "https://api.example.com/v1/users" + ``` + +- [#6019](https://github.com/Effect-TS/effect/pull/6019) [`9a96b87`](https://github.com/Effect-TS/effect/commit/9a96b87a33a75ebc277c585e60758ab4409c0d9e) Thanks @codewithkenzo! - Fix `retryTransient` to use correct transient status codes + + Changed `isTransientResponse` from `status >= 429` to an explicit allowlist (408, 429, 500, 502, 503, 504). This correctly excludes 501 (Not Implemented) and 505+ permanent errors, while including 408 (Request Timeout) which was previously missed. + + Also aligned response retry behavior with v4: the `while` predicate now only applies to error retries, not response retries. Response retries are determined solely by `isTransientResponse`. This matches the semantic intent since `while` is typed for errors, not responses. + + Fixes #5995 + +- Updated dependencies [[`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49)]: + - effect@3.19.16 + ## 0.94.2 ### Patch Changes diff --git a/packages/platform/package.json b/packages/platform/package.json index fb9e07f2122..5111033bd1b 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform", "type": "module", - "version": "0.94.2", + "version": "0.96.1", "license": "MIT", "description": "Unified interfaces for common platform-specific services", "homepage": "https://effect.website", @@ -48,7 +48,7 @@ }, "dependencies": { "find-my-way-ts": "^0.1.6", - "msgpackr": "^1.11.4", + "msgpackr": "^1.11.10", "multipasta": "^0.2.7" }, "peerDependencies": { diff --git a/packages/platform/src/HttpBody.ts b/packages/platform/src/HttpBody.ts index b9487aac39c..0b59c458835 100644 --- a/packages/platform/src/HttpBody.ts +++ b/packages/platform/src/HttpBody.ts @@ -165,13 +165,13 @@ export const text: (body: string, contentType?: string) => Uint8Array = internal * @since 1.0.0 * @category constructors */ -export const unsafeJson: (body: unknown) => Uint8Array = internal.unsafeJson +export const unsafeJson: (body: unknown, contentType?: string) => Uint8Array = internal.unsafeJson /** * @since 1.0.0 * @category constructors */ -export const json: (body: unknown) => Effect.Effect = internal.json +export const json: (body: unknown, contentType?: string) => Effect.Effect = internal.json /** * @since 1.0.0 @@ -179,13 +179,13 @@ export const json: (body: unknown) => Effect.Effect = */ export const jsonSchema: ( schema: Schema.Schema -) => (body: A) => Effect.Effect = internal.jsonSchema +) => (body: A, contentType?: string) => Effect.Effect = internal.jsonSchema /** * @since 1.0.0 * @category constructors */ -export const urlParams: (urlParams: UrlParams.UrlParams) => Uint8Array = internal.urlParams +export const urlParams: (urlParams: UrlParams.UrlParams, contentType?: string) => Uint8Array = internal.urlParams /** * @since 1.0.0 diff --git a/packages/platform/src/HttpClient.ts b/packages/platform/src/HttpClient.ts index 0a8a2f58817..628e9e52296 100644 --- a/packages/platform/src/HttpClient.ts +++ b/packages/platform/src/HttpClient.ts @@ -534,7 +534,7 @@ export const retryTransient: { >( options: { readonly mode?: Mode | undefined - readonly while?: Predicate.Predicate> + readonly while?: Predicate.Predicate> readonly schedule?: Schedule.Schedule, R1> readonly times?: number } | Schedule.Schedule, R1> @@ -552,7 +552,7 @@ export const retryTransient: { self: HttpClient.With, options: { readonly mode?: Mode | undefined - readonly while?: Predicate.Predicate> + readonly while?: Predicate.Predicate> readonly schedule?: Schedule.Schedule, R1> readonly times?: number } | Schedule.Schedule, R1> diff --git a/packages/platform/src/HttpLayerRouter.ts b/packages/platform/src/HttpLayerRouter.ts index fba772bc013..64032471e76 100644 --- a/packages/platform/src/HttpLayerRouter.ts +++ b/packages/platform/src/HttpLayerRouter.ts @@ -1032,7 +1032,7 @@ export const addHttpApi = Context.merge(context, input)) })) } diff --git a/packages/platform/src/HttpServerResponse.ts b/packages/platform/src/HttpServerResponse.ts index d5ffa152cc1..72b7c64e8cf 100644 --- a/packages/platform/src/HttpServerResponse.ts +++ b/packages/platform/src/HttpServerResponse.ts @@ -142,8 +142,10 @@ export const json: ( export const schemaJson: ( schema: Schema.Schema, options?: ParseOptions | undefined -) => (body: A, options?: Options.WithContent | undefined) => Effect.Effect = - internal.schemaJson +) => ( + body: A, + options?: Options.WithContentType | undefined +) => Effect.Effect = internal.schemaJson /** * @since 1.0.0 diff --git a/packages/platform/src/internal/httpBody.ts b/packages/platform/src/internal/httpBody.ts index 372e66da2cd..f43cab39490 100644 --- a/packages/platform/src/internal/httpBody.ts +++ b/packages/platform/src/internal/httpBody.ts @@ -114,26 +114,27 @@ export const text = (body: string, contentType?: string): Body.Uint8Array => uint8Array(encoder.encode(body), contentType ?? "text/plain") /** @internal */ -export const unsafeJson = (body: unknown): Body.Uint8Array => text(JSON.stringify(body), "application/json") +export const unsafeJson = (body: unknown, contentType?: string): Body.Uint8Array => + text(JSON.stringify(body), contentType ?? "application/json") /** @internal */ -export const json = (body: unknown): Effect.Effect => +export const json = (body: unknown, contentType?: string): Effect.Effect => Effect.try({ - try: () => unsafeJson(body), + try: () => unsafeJson(body, contentType), catch: (error) => HttpBodyError({ _tag: "JsonError", error }) }) /** @internal */ -export const urlParams = (urlParams: UrlParams.UrlParams): Body.Uint8Array => - text(UrlParams.toString(urlParams), "application/x-www-form-urlencoded") +export const urlParams = (urlParams: UrlParams.UrlParams, contentType?: string): Body.Uint8Array => + text(UrlParams.toString(urlParams), contentType ?? "application/x-www-form-urlencoded") /** @internal */ export const jsonSchema = (schema: Schema.Schema, options?: ParseOptions) => { const encode = Schema.encode(schema, options) - return (body: A): Effect.Effect => + return (body: A, contentType?: string): Effect.Effect => Effect.flatMap( Effect.mapError(encode(body), (error) => HttpBodyError({ _tag: "SchemaError", error })), - json + (body) => json(body, contentType) ) } diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index 19b9276f2ab..503016040f5 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -755,7 +755,7 @@ export const retryTransient: { >( options: { readonly mode?: Mode | undefined - readonly while?: Predicate.Predicate> + readonly while?: Predicate.Predicate> readonly schedule?: Schedule.Schedule, R1> readonly times?: number } | Schedule.Schedule, R1> @@ -773,7 +773,7 @@ export const retryTransient: { self: Client.HttpClient.With, options: { readonly mode?: Mode | undefined - readonly while?: Predicate.Predicate> + readonly while?: Predicate.Predicate> readonly schedule?: Schedule.Schedule, R1> readonly times?: number } | Schedule.Schedule, R1> @@ -784,7 +784,7 @@ export const retryTransient: { self: Client.HttpClient.With, options: { readonly mode?: "errors-only" | "response-only" | "both" | undefined - readonly while?: Predicate.Predicate> + readonly while?: Predicate.Predicate> readonly schedule?: Schedule.Schedule, R1> readonly times?: number } | Schedule.Schedule, R1> @@ -800,9 +800,7 @@ export const retryTransient: { mode === "errors-only" ? identity : Effect.repeat({ schedule: passthroughSchedule, times, - while: isOnlySchedule || options.while === undefined - ? isTransientResponse - : Predicate.and(isTransientResponse, options.while) + while: isTransientResponse }), mode === "response-only" ? identity : Effect.retry({ while: isOnlySchedule || options.while === undefined @@ -824,7 +822,13 @@ const isTransientHttpError = (error: unknown) => ((error._tag === "RequestError" && error.reason === "Transport") || (error._tag === "ResponseError" && isTransientResponse(error.response))) -const isTransientResponse = (response: ClientResponse.HttpClientResponse) => response.status >= 429 +const isTransientResponse = (response: ClientResponse.HttpClientResponse) => + response.status === 408 || + response.status === 429 || + response.status === 500 || + response.status === 502 || + response.status === 503 || + response.status === 504 /** @internal */ export const tap = dual< diff --git a/packages/platform/src/internal/httpClientRequest.ts b/packages/platform/src/internal/httpClientRequest.ts index 73d0132ce2a..14fb718c526 100644 --- a/packages/platform/src/internal/httpClientRequest.ts +++ b/packages/platform/src/internal/httpClientRequest.ts @@ -250,17 +250,21 @@ export const setUrl = dual< export const appendUrl = dual< (path: string) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest, (self: ClientRequest.HttpClientRequest, path: string) => ClientRequest.HttpClientRequest ->(2, (self, url) => - makeInternal( +>(2, (self, path) => { + if (path === "") { + return self + } + const baseUrl = self.url.endsWith("/") ? self.url : self.url + "/" + const pathSegment = path.startsWith("/") ? path.slice(1) : path + return makeInternal( self.method, - self.url.endsWith("/") && url.startsWith("/") ? - self.url + url.slice(1) : - self.url + url, + baseUrl + pathSegment, self.urlParams, self.hash, self.headers, self.body - )) + ) +}) /** @internal */ export const prependUrl = dual< diff --git a/packages/platform/src/internal/httpServerError.ts b/packages/platform/src/internal/httpServerError.ts index bee8841f22a..a71639edd61 100644 --- a/packages/platform/src/internal/httpServerError.ts +++ b/packages/platform/src/internal/httpServerError.ts @@ -28,47 +28,49 @@ export const clientAbortFiberId = globalValue( export const causeResponse = ( cause: Cause.Cause ): Effect.Effect]> => { - const [effect, stripped] = Cause.reduce( - cause, - [Effect.succeed(internalServerError), Cause.empty as Cause.Cause] as const, - (acc, cause) => { - const withoutInterrupt = Cause.isInterruptType(acc[1]) ? Cause.empty : acc[1] - switch (cause._tag) { - case "Fail": { - return Option.some( - [ - Respondable.toResponseOrElse(cause.error, internalServerError), - combineCauses(withoutInterrupt, cause) - ] as const - ) - } - case "Die": { - const isResponse = internalServerResponse.isServerResponse(cause.defect) - return Option.some( - [ - Respondable.toResponseOrElseDefect(cause.defect, internalServerError), - isResponse ? withoutInterrupt : combineCauses(withoutInterrupt, cause) - ] as const - ) - } - case "Interrupt": { - if (acc[1]._tag !== "Empty") { - return Option.none() - } - const response = cause.fiberId === clientAbortFiberId ? clientAbortError : serverAbortError - return Option.some([Effect.succeed(response), cause] as const) - } - default: { - return Option.none() + let effect: Effect.Effect = Effect.succeed(internalServerError) + let stripped: Cause.Cause = Cause.empty as Cause.Cause + let isClientInterrupt = false + let hasResponse = false + + Cause.reduce(cause, void 0 as void, (_, current) => { + const withoutInterrupt = Cause.isInterruptType(stripped) ? Cause.empty : stripped + switch (current._tag) { + case "Fail": { + effect = Respondable.toResponseOrElse(current.error, internalServerError) + stripped = combineCauses(withoutInterrupt, current) + break + } + case "Die": { + const isResponse = internalServerResponse.isServerResponse(current.defect) + effect = Respondable.toResponseOrElseDefect(current.defect, internalServerError) + stripped = isResponse ? withoutInterrupt : combineCauses(withoutInterrupt, current) + hasResponse = hasResponse || isResponse + break + } + case "Interrupt": { + isClientInterrupt = isClientInterrupt || current.fiberId === clientAbortFiberId + if (Cause.isEmptyType(stripped) && !hasResponse) { + stripped = current } + break } } - ) - return Effect.map(effect, (response) => { - if (Cause.isEmptyType(stripped)) { + return Option.none() + }) + + const responseEffect = !hasResponse && Cause.isInterruptType(stripped) + ? Effect.succeed(isClientInterrupt ? clientAbortError : serverAbortError) + : effect + const strippedCause: Cause.Cause = !hasResponse && Cause.isInterruptType(stripped) && isClientInterrupt + ? Cause.interrupt(clientAbortFiberId) as Cause.Cause + : stripped + + return Effect.map(responseEffect, (response) => { + if (Cause.isEmptyType(strippedCause)) { return [response, Cause.empty] as const } - return [response, Cause.sequential(stripped, Cause.die(response))] as const + return [response, Cause.sequential(strippedCause, Cause.die(response))] as const }) } diff --git a/packages/platform/src/internal/httpServerResponse.ts b/packages/platform/src/internal/httpServerResponse.ts index 3759cd9c451..04ce41e9b3a 100644 --- a/packages/platform/src/internal/httpServerResponse.ts +++ b/packages/platform/src/internal/httpServerResponse.ts @@ -195,29 +195,33 @@ export const htmlStream = => - Effect.map(internalBody.json(body), (body) => + options?: ServerResponse.Options.WithContentType | undefined +): Effect.Effect => { + const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty + return Effect.map(internalBody.json(body, getContentType(options, headers)), (body) => new ServerResponseImpl( options?.status ?? 200, options?.statusText, - options?.headers ? Headers.fromInput(options.headers) : Headers.empty, + headers, options?.cookies ?? Cookies.empty, body )) +} /** @internal */ export const unsafeJson = ( body: unknown, - options?: ServerResponse.Options.WithContent | undefined -): ServerResponse.HttpServerResponse => - new ServerResponseImpl( + options?: ServerResponse.Options.WithContentType | undefined +): ServerResponse.HttpServerResponse => { + const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty + return new ServerResponseImpl( options?.status ?? 200, options?.statusText, - options?.headers ? Headers.fromInput(options.headers) : Headers.empty, + headers, options?.cookies ?? Cookies.empty, - internalBody.unsafeJson(body) + internalBody.unsafeJson(body, getContentType(options, headers)) ) +} /** @internal */ export const schemaJson = ( @@ -227,16 +231,18 @@ export const schemaJson = ( const encode = internalBody.jsonSchema(schema, options) return ( body: A, - options?: ServerResponse.Options.WithContent | undefined - ): Effect.Effect => - Effect.map(encode(body), (body) => + options?: ServerResponse.Options.WithContentType | undefined + ): Effect.Effect => { + const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty + return Effect.map(encode(body, getContentType(options, headers)), (body) => new ServerResponseImpl( options?.status ?? 200, options?.statusText, - options?.headers ? Headers.fromInput(options.headers) : Headers.empty, + headers, options?.cookies ?? Cookies.empty, body )) + } } const httpPlatform = Context.GenericTag("@effect/platform/HttpPlatform") @@ -264,15 +270,20 @@ export const fileWeb = ( /** @internal */ export const urlParams = ( body: UrlParams.Input, - options?: ServerResponse.Options.WithContent | undefined -): ServerResponse.HttpServerResponse => - new ServerResponseImpl( + options?: ServerResponse.Options.WithContentType | undefined +): ServerResponse.HttpServerResponse => { + const headers = options?.headers ? Headers.fromInput(options.headers) : Headers.empty + return new ServerResponseImpl( options?.status ?? 200, options?.statusText, - options?.headers ? Headers.fromInput(options.headers) : Headers.empty, + headers, options?.cookies ?? Cookies.empty, - internalBody.text(UrlParams.toString(UrlParams.fromInput(body)), "application/x-www-form-urlencoded") + internalBody.urlParams( + UrlParams.fromInput(body), + getContentType(options, headers) + ) ) +} /** @internal */ export const raw = (body: unknown, options?: ServerResponse.Options | undefined): ServerResponse.HttpServerResponse => diff --git a/packages/platform/src/internal/workerRunner.ts b/packages/platform/src/internal/workerRunner.ts index f0143d59372..09fb01dec9b 100644 --- a/packages/platform/src/internal/workerRunner.ts +++ b/packages/platform/src/internal/workerRunner.ts @@ -47,9 +47,13 @@ export const make = Effect.fnUntraced(function*( yield* Deferred.await(closeLatch).pipe( Effect.onExit(() => { - fiber.currentScheduler.scheduleTask(() => { - fiber.unsafeInterruptAsFork(fiber.id()) - }, 0) + fiber.currentScheduler.scheduleTask( + () => { + fiber.unsafeInterruptAsFork(fiber.id()) + }, + 0, + fiber + ) return Effect.void }), Effect.forkScoped diff --git a/packages/platform/test/HttpApp.test.ts b/packages/platform/test/HttpApp.test.ts index c8291f538c5..9cc04abb964 100644 --- a/packages/platform/test/HttpApp.test.ts +++ b/packages/platform/test/HttpApp.test.ts @@ -1,7 +1,7 @@ import { HttpApp, HttpServerResponse } from "@effect/platform" import { describe, test } from "@effect/vitest" import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" -import { Context, Effect, FiberRef, Runtime, Stream } from "effect" +import { Context, Effect, FiberRef, Runtime, Schema, Stream } from "effect" import * as Layer from "effect/Layer" describe("Http/App", () => { @@ -20,6 +20,38 @@ describe("Http/App", () => { strictEqual(response.headers.get("Content-Type"), "application/json") }) + test("json supports contentType option", async () => { + const handler = HttpApp.toWebHandler( + HttpServerResponse.json({ foo: "bar" }, { contentType: "application/problem+json" }) + ) + const response = await handler(new Request("http://localhost:3000/")) + strictEqual(response.headers.get("Content-Type"), "application/problem+json") + }) + + test("json supports content-type header", async () => { + const handler = HttpApp.toWebHandler( + HttpServerResponse.json({ foo: "bar" }, { headers: { "content-type": "application/vnd.api+json" } }) + ) + const response = await handler(new Request("http://localhost:3000/")) + strictEqual(response.headers.get("Content-Type"), "application/vnd.api+json") + }) + + test("schemaJson supports contentType option", async () => { + const encode = HttpServerResponse.schemaJson(Schema.Struct({ foo: Schema.String })) + const handler = HttpApp.toWebHandler(encode({ foo: "bar" }, { contentType: "application/merge-patch+json" })) + const response = await handler(new Request("http://localhost:3000/")) + strictEqual(response.headers.get("Content-Type"), "application/merge-patch+json") + }) + + test("urlParams supports contentType option", async () => { + const handler = HttpApp.toWebHandler( + HttpServerResponse.urlParams({ foo: "bar" }, { contentType: "text/plain" }) + ) + const response = await handler(new Request("http://localhost:3000/")) + strictEqual(response.headers.get("Content-Type"), "text/plain") + strictEqual(await response.text(), "foo=bar") + }) + test("cookies", async () => { const handler = HttpApp.toWebHandler( HttpServerResponse.unsafeJson({ foo: "bar" }).pipe( diff --git a/packages/platform/test/HttpBody.test.ts b/packages/platform/test/HttpBody.test.ts new file mode 100644 index 00000000000..34010f0af5a --- /dev/null +++ b/packages/platform/test/HttpBody.test.ts @@ -0,0 +1,27 @@ +import { HttpBody, UrlParams } from "@effect/platform" +import { describe, test } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Effect, Schema } from "effect" + +describe("HttpBody", () => { + test("json supports contentType", async () => { + const body = await Effect.runPromise(HttpBody.json({ foo: "bar" }, "application/problem+json")) + strictEqual(body.contentType, "application/problem+json") + }) + + test("unsafeJson supports contentType", () => { + const body = HttpBody.unsafeJson({ foo: "bar" }, "application/vnd.api+json") + strictEqual(body.contentType, "application/vnd.api+json") + }) + + test("jsonSchema supports contentType", async () => { + const encode = HttpBody.jsonSchema(Schema.Struct({ foo: Schema.String })) + const body = await Effect.runPromise(encode({ foo: "bar" }, "application/merge-patch+json")) + strictEqual(body.contentType, "application/merge-patch+json") + }) + + test("urlParams supports contentType", () => { + const body = HttpBody.urlParams(UrlParams.fromInput({ foo: "bar" }), "text/plain") + strictEqual(body.contentType, "text/plain") + }) +}) diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index 711bd46782f..ca1c6067343 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -202,6 +202,50 @@ describe("HttpClient", () => { ) }) + describe("appendUrl", () => { + it("joins path without trailing slash on base", () => { + const request = HttpClientRequest.get("https://api.example.com/v1").pipe( + HttpClientRequest.appendUrl("users") + ) + strictEqual(request.url, "https://api.example.com/v1/users") + }) + + it("joins path with trailing slash on base", () => { + const request = HttpClientRequest.get("https://api.example.com/v1/").pipe( + HttpClientRequest.appendUrl("users") + ) + strictEqual(request.url, "https://api.example.com/v1/users") + }) + + it("joins path with leading slash", () => { + const request = HttpClientRequest.get("https://api.example.com/v1").pipe( + HttpClientRequest.appendUrl("/users") + ) + strictEqual(request.url, "https://api.example.com/v1/users") + }) + + it("joins path with both trailing and leading slashes", () => { + const request = HttpClientRequest.get("https://api.example.com/v1/").pipe( + HttpClientRequest.appendUrl("/users") + ) + strictEqual(request.url, "https://api.example.com/v1/users") + }) + + it("joins nested paths", () => { + const request = HttpClientRequest.get("https://api.example.com/v1").pipe( + HttpClientRequest.appendUrl("users/123/posts") + ) + strictEqual(request.url, "https://api.example.com/v1/users/123/posts") + }) + + it("handles empty path", () => { + const request = HttpClientRequest.get("https://api.example.com/v1").pipe( + HttpClientRequest.appendUrl("") + ) + strictEqual(request.url, "https://api.example.com/v1") + }) + }) + it.effect("matchStatus", () => Effect.gen(function*() { const jp = yield* JsonPlaceholder @@ -261,4 +305,111 @@ describe("HttpClient", () => { Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }) ), 30000) + + describe("retryTransient", () => { + const makeTestClient = (status: number) => { + const attemptsRef = Ref.unsafeMake(0) + const client = HttpClient.make((request) => + Effect.gen(function*() { + yield* Ref.update(attemptsRef, (n) => n + 1) + return HttpClientResponse.fromWeb(request, new Response(null, { status })) + }) + ) + return { attemptsRef, client } + } + + it.effect("retries 408 Request Timeout", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(408) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 3) + })) + + it.effect("retries 429 Too Many Requests", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(429) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 3) + })) + + it.effect("retries 500 Internal Server Error", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(500) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 3) + })) + + it.effect("retries 502 Bad Gateway", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(502) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 3) + })) + + it.effect("retries 503 Service Unavailable", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(503) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 3) + })) + + it.effect("retries 504 Gateway Timeout", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(504) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 3) + })) + + it.effect("does NOT retry 200 OK", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(200) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 1) + })) + + it.effect("does NOT retry 501 Not Implemented", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(501) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 1) + })) + + it.effect("does NOT retry 505 HTTP Version Not Supported", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(505) + const retryClient = client.pipe(HttpClient.retryTransient({ times: 2 })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 1) + })) + + it.effect("while predicate only applies to error retries, not response retries", () => + Effect.gen(function*() { + const { attemptsRef, client } = makeTestClient(503) + const errorOnlyPredicate = (e: unknown) => e !== null && typeof e === "object" && "_tag" in e + const retryClient = client.pipe(HttpClient.retryTransient({ + times: 2, + while: errorOnlyPredicate + })) + yield* retryClient.get("http://test/").pipe(Effect.ignore) + const attempts = yield* Ref.get(attemptsRef) + strictEqual(attempts, 3) + })) + }) }) diff --git a/packages/printer-ansi/CHANGELOG.md b/packages/printer-ansi/CHANGELOG.md index 2185a6cc1ec..6c9ae2929fa 100644 --- a/packages/printer-ansi/CHANGELOG.md +++ b/packages/printer-ansi/CHANGELOG.md @@ -1,5 +1,23 @@ # @effect/printer-ansi +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/printer@0.49.0 + - @effect/typeclass@0.40.0 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/printer@0.48.0 + - @effect/typeclass@0.39.0 + ## 0.47.0 ### Patch Changes diff --git a/packages/printer-ansi/package.json b/packages/printer-ansi/package.json index cb01b8babd8..b0563552b91 100644 --- a/packages/printer-ansi/package.json +++ b/packages/printer-ansi/package.json @@ -1,6 +1,6 @@ { "name": "@effect/printer-ansi", - "version": "0.47.0", + "version": "0.49.0", "type": "module", "license": "MIT", "description": "An easy to use, extensible pretty-printer for rendering documents for the terminal", diff --git a/packages/printer/CHANGELOG.md b/packages/printer/CHANGELOG.md index 3c8a32f1513..0cbb47cfd4c 100644 --- a/packages/printer/CHANGELOG.md +++ b/packages/printer/CHANGELOG.md @@ -1,5 +1,21 @@ # @effect/printer +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/typeclass@0.40.0 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/typeclass@0.39.0 + ## 0.47.0 ### Patch Changes diff --git a/packages/printer/package.json b/packages/printer/package.json index 47c343c0191..e947be4029c 100644 --- a/packages/printer/package.json +++ b/packages/printer/package.json @@ -1,6 +1,6 @@ { "name": "@effect/printer", - "version": "0.47.0", + "version": "0.49.0", "type": "module", "license": "MIT", "description": "An easy to use, extensible pretty-printer for rendering documents", diff --git a/packages/rpc/CHANGELOG.md b/packages/rpc/CHANGELOG.md index 6f2343b3c04..a0cee881636 100644 --- a/packages/rpc/CHANGELOG.md +++ b/packages/rpc/CHANGELOG.md @@ -1,5 +1,54 @@ # @effect/rpc +## 0.75.1 + +### Patch Changes + +- [#6191](https://github.com/Effect-TS/effect/pull/6191) [`c016642`](https://github.com/Effect-TS/effect/commit/c0166426f80b7eb8e7f7d3aecc95dcd4fdb5cb55) Thanks @IGassmann! - Update `msgpackr` to 1.11.10 to fix silent decode failures in environments that block `new Function()` at runtime (e.g. Cloudflare Workers). The new version wraps the JIT `new Function()` call in a try/catch, falling back to the interpreted path when dynamic code evaluation is blocked. + +- [#6110](https://github.com/Effect-TS/effect/pull/6110) [`0fac630`](https://github.com/Effect-TS/effect/commit/0fac630b27095ffbfa6c48851087950ddc29cda0) Thanks @mitre88! - fix: correct typos in source code (receive, separate) + +- [#6161](https://github.com/Effect-TS/effect/pull/6161) [`e2374c2`](https://github.com/Effect-TS/effect/commit/e2374c20ce699d9f5340baf744cf1bd67bb220a0) Thanks @bohdanbirdie! - add RpcSerialization.makeMsgPack + +- Updated dependencies [[`74f3267`](https://github.com/Effect-TS/effect/commit/74f3267a6cc7ed7818c4c34cc1232f7cfc7d3339), [`518d0e3`](https://github.com/Effect-TS/effect/commit/518d0e3f4879be6d9d9a7fa137a1820604bb3ea7), [`c016642`](https://github.com/Effect-TS/effect/commit/c0166426f80b7eb8e7f7d3aecc95dcd4fdb5cb55)]: + - effect@3.21.2 + - @effect/platform@0.96.1 + +## 0.75.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/platform@0.96.0 + +## 0.74.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/platform@0.95.0 + +## 0.73.2 + +### Patch Changes + +- [#6065](https://github.com/Effect-TS/effect/pull/6065) [`94b00c8`](https://github.com/Effect-TS/effect/commit/94b00c8e4c5c150f858c95262d0ff1433276ede5) Thanks @marbemac! - Add optional `defect` parameter to `Rpc.make` for customizing defect serialization per-RPC. Defaults to `Schema.Defect`, preserving existing behavior. + +- Updated dependencies [[`12b1f1e`](https://github.com/Effect-TS/effect/commit/12b1f1eadf649e30dec581b7351ba3abb12f8004)]: + - effect@3.19.18 + +## 0.73.1 + +### Patch Changes + +- [#6055](https://github.com/Effect-TS/effect/pull/6055) [`598ff76`](https://github.com/Effect-TS/effect/commit/598ff7642fdee7f3379bca49e378a0e9647bbe75) Thanks @marbemac! - Fix `sendRequestDefect` and `sendDefect` to encode defects with `Schema.Defect`, preventing `Error` objects from being serialized as `{}` due to non-enumerable properties. + +- Updated dependencies [[`d67c708`](https://github.com/Effect-TS/effect/commit/d67c7089ba8616b2d48ef7324312267a2a6f310a), [`a8c436f`](https://github.com/Effect-TS/effect/commit/a8c436f7004cc2a8ce2daec589ea7256b91c324f)]: + - @effect/platform@0.94.5 + - effect@3.19.17 + ## 0.73.0 ### Patch Changes diff --git a/packages/rpc/package.json b/packages/rpc/package.json index 4312a49ae6a..226e0dd0e86 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -1,6 +1,6 @@ { "name": "@effect/rpc", - "version": "0.73.0", + "version": "0.75.1", "type": "module", "license": "MIT", "description": "Functional programming in TypeScript", @@ -54,6 +54,6 @@ "effect": "workspace:^" }, "dependencies": { - "msgpackr": "^1.11.4" + "msgpackr": "^1.11.10" } } diff --git a/packages/rpc/src/Rpc.ts b/packages/rpc/src/Rpc.ts index 147c46be83e..57a04869fba 100644 --- a/packages/rpc/src/Rpc.ts +++ b/packages/rpc/src/Rpc.ts @@ -58,6 +58,7 @@ export interface Rpc< readonly payloadSchema: Payload readonly successSchema: Success readonly errorSchema: Error + readonly defectSchema: Schema.Schema readonly annotations: Context_.Context readonly middlewares: ReadonlySet @@ -171,6 +172,7 @@ export interface AnyWithProps { readonly payloadSchema: AnySchema readonly successSchema: Schema.Schema.Any readonly errorSchema: Schema.Schema.All + readonly defectSchema: Schema.Schema readonly annotations: Context_.Context readonly middlewares: ReadonlySet } @@ -541,6 +543,7 @@ const Proto = { payloadSchema: this.payloadSchema, successSchema, errorSchema: this.errorSchema, + defectSchema: this.defectSchema, annotations: this.annotations, middlewares: this.middlewares }) @@ -551,6 +554,7 @@ const Proto = { payloadSchema: this.payloadSchema, successSchema: this.successSchema, errorSchema, + defectSchema: this.defectSchema, annotations: this.annotations, middlewares: this.middlewares }) @@ -561,6 +565,7 @@ const Proto = { payloadSchema: Schema.isSchema(payloadSchema) ? payloadSchema as any : Schema.Struct(payloadSchema as any), successSchema: this.successSchema, errorSchema: this.errorSchema, + defectSchema: this.defectSchema, annotations: this.annotations, middlewares: this.middlewares }) @@ -571,6 +576,7 @@ const Proto = { payloadSchema: this.payloadSchema, successSchema: this.successSchema, errorSchema: this.errorSchema, + defectSchema: this.defectSchema, annotations: this.annotations, middlewares: new Set([...this.middlewares, middleware]) }) @@ -581,6 +587,7 @@ const Proto = { payloadSchema: this.payloadSchema, successSchema: this.successSchema, errorSchema: this.errorSchema, + defectSchema: this.defectSchema, annotations: this.annotations, middlewares: this.middlewares }) @@ -591,6 +598,7 @@ const Proto = { payloadSchema: this.payloadSchema, successSchema: this.successSchema, errorSchema: this.errorSchema, + defectSchema: this.defectSchema, middlewares: this.middlewares, annotations: Context_.add(this.annotations, tag, value) }) @@ -601,6 +609,7 @@ const Proto = { payloadSchema: this.payloadSchema, successSchema: this.successSchema, errorSchema: this.errorSchema, + defectSchema: this.defectSchema, middlewares: this.middlewares, annotations: Context_.merge(this.annotations, context) }) @@ -618,6 +627,7 @@ const makeProto = < readonly payloadSchema: Payload readonly successSchema: Success readonly errorSchema: Error + readonly defectSchema: Schema.Schema readonly annotations: Context_.Context readonly middlewares: ReadonlySet }): Rpc => { @@ -643,6 +653,7 @@ export const make = < readonly success?: Success readonly error?: Error readonly stream?: Stream + readonly defect?: Schema.Schema readonly primaryKey?: [Payload] extends [Schema.Struct.Fields] ? ((payload: Schema.Simplify>>) => string) : never @@ -678,6 +689,7 @@ export const make = < }) : successSchema, errorSchema: options?.stream ? Schema.Never : errorSchema, + defectSchema: options?.defect ?? Schema.Defect, annotations: Context_.empty(), middlewares: new Set() }) as any @@ -719,6 +731,7 @@ export const fromTaggedRequest = ( payloadSchema: schema as any, successSchema: schema.success as any, errorSchema: schema.failure, + defectSchema: Schema.Defect, annotations: Context_.empty(), middlewares: new Set() }) @@ -747,7 +760,7 @@ export const exitSchema = ( const schema = Schema.Exit({ success: Option.isSome(streamSchemas) ? Schema.Void : rpc.successSchema, failure: Schema.Union(...failures), - defect: Schema.Defect + defect: rpc.defectSchema }) exitSchemaCache.set(self, schema) return schema as any diff --git a/packages/rpc/src/RpcClient.ts b/packages/rpc/src/RpcClient.ts index fb6ecbc72ec..1b5e59b4661 100644 --- a/packages/rpc/src/RpcClient.ts +++ b/packages/rpc/src/RpcClient.ts @@ -376,9 +376,13 @@ export const makeNoSerialization: { - fiber.unsafeInterruptAsFork(parentFiber.id()) - }, 0) + parentFiber.currentScheduler.scheduleTask( + () => { + fiber.unsafeInterruptAsFork(parentFiber.id()) + }, + 0, + fiber + ) } } } @@ -1045,18 +1049,18 @@ const defaultRetrySchedule = Schedule.exponential(500, 1.5).pipe( ) const makePinger = Effect.fnUntraced(function*(writePing: Effect.Effect) { - let recievedPong = true + let receivedPong = true const latch = Effect.unsafeMakeLatch() const reset = () => { - recievedPong = true + receivedPong = true latch.unsafeClose() } const onPong = () => { - recievedPong = true + receivedPong = true } yield* Effect.suspend(() => { - if (!recievedPong) return latch.open - recievedPong = false + if (!receivedPong) return latch.open + receivedPong = false return writePing }).pipe( Effect.delay("10 seconds"), diff --git a/packages/rpc/src/RpcSerialization.ts b/packages/rpc/src/RpcSerialization.ts index 4286c3e1c4b..6546511fc5f 100644 --- a/packages/rpc/src/RpcSerialization.ts +++ b/packages/rpc/src/RpcSerialization.ts @@ -352,43 +352,52 @@ interface JsonRpcResponse { type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse /** + * Create a MessagePack serialization with custom msgpackr options. + * * @since 1.0.0 * @category serialization */ -export const msgPack: RpcSerialization["Type"] = RpcSerialization.of({ - contentType: "application/msgpack", - includesFraming: true, - unsafeMake: () => { - const unpackr = new Msgpackr.Unpackr() - const packr = new Msgpackr.Packr() - const encoder = new TextEncoder() - let incomplete: Uint8Array | undefined = undefined - return { - decode: (bytes) => { - let buf = typeof bytes === "string" ? encoder.encode(bytes) : bytes - if (incomplete !== undefined) { - const prev = buf - bytes = new Uint8Array(incomplete.length + buf.length) - bytes.set(incomplete) - bytes.set(prev, incomplete.length) - buf = bytes - incomplete = undefined - } - try { - return unpackr.unpackMultiple(buf) - } catch (error_) { - const error = error_ as any - if (error.incomplete) { - incomplete = buf.subarray(error.lastPosition) - return error.values ?? [] +export const makeMsgPack = (options?: Msgpackr.Options | undefined): RpcSerialization["Type"] => + RpcSerialization.of({ + contentType: "application/msgpack", + includesFraming: true, + unsafeMake() { + const unpackr = new Msgpackr.Unpackr(options) + const packr = new Msgpackr.Packr(options) + const encoder = new TextEncoder() + let incomplete: Uint8Array | undefined = undefined + return { + decode(bytes) { + let buf = typeof bytes === "string" ? encoder.encode(bytes) : bytes + if (incomplete !== undefined) { + const prev = buf + bytes = new Uint8Array(incomplete.length + buf.length) + bytes.set(incomplete) + bytes.set(prev, incomplete.length) + buf = bytes + incomplete = undefined } - return [] - } - }, - encode: (response) => packr.pack(response) + try { + return unpackr.unpackMultiple(buf) + } catch (error_) { + const error = error_ as any + if (error.incomplete) { + incomplete = buf.subarray(error.lastPosition) + return error.values ?? [] + } + throw error_ + } + }, + encode: (response) => packr.pack(response) + } } - } -}) + }) + +/** + * @since 1.0.0 + * @category serialization + */ +export const msgPack: RpcSerialization["Type"] = makeMsgPack({ useRecords: true }) /** * A rpc serialization layer that uses JSON for serialization. @@ -423,7 +432,7 @@ export const layerJsonRpc = (options?: { }): Layer.Layer => Layer.succeed(RpcSerialization, jsonRpc(options)) /** - * A rpc serialization layer that uses JSON-RPC for serialization seperated by + * A rpc serialization layer that uses JSON-RPC for serialization separated by * new lines. * * @since 1.0.0 diff --git a/packages/rpc/src/RpcServer.ts b/packages/rpc/src/RpcServer.ts index b8ff2725072..5bf0d40b0f5 100644 --- a/packages/rpc/src/RpcServer.ts +++ b/packages/rpc/src/RpcServer.ts @@ -510,6 +510,7 @@ export const make: ( return handleEncode( client, response.requestId, + schemas.encodeDefect, schemas.collector, Effect.provide(schemas.encodeChunk(response.values), schemas.context), (values) => ({ _tag: "Chunk", requestId: String(response.requestId), values }) @@ -522,6 +523,7 @@ export const make: ( return handleEncode( client, response.requestId, + schemas.encodeDefect, schemas.collector, Effect.provide(schemas.encodeExit(response.exit), schemas.context), (exit) => ({ _tag: "Exit", requestId: String(response.requestId), exit }) @@ -552,6 +554,7 @@ export const make: ( readonly decode: (u: unknown) => Effect.Effect, ParseError> readonly encodeChunk: (u: ReadonlyArray) => Effect.Effect, ParseError> readonly encodeExit: (u: unknown) => Effect.Effect, ParseError> + readonly encodeDefect: (u: unknown) => Effect.Effect readonly context: Context.Context readonly collector?: Transferable.CollectorService | undefined } @@ -568,6 +571,7 @@ export const make: ( Schema.Array(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any) ) as any, encodeExit: Schema.encodeUnknown(Rpc.exitSchema(rpc as any)) as any, + encodeDefect: Schema.encodeUnknown(rpc.defectSchema) as any, context: entry.context } schemasCache.set(rpc, schemas) @@ -584,6 +588,7 @@ export const make: ( const handleEncode = ( client: Client, requestId: RequestId, + encodeDefect: (u: unknown) => Effect.Effect, collector: Transferable.CollectorService | undefined, effect: Effect.Effect, onSuccess: (a: A) => FromServerEncoded @@ -594,31 +599,40 @@ export const make: ( client.schemas.delete(requestId) const defect = Cause.squash(Cause.map(cause, TreeFormatter.formatErrorSync)) return Effect.zipRight( - sendRequestDefect(client, requestId, defect), + sendRequestDefect(client, requestId, encodeDefect, defect), server.write(client.id, { _tag: "Interrupt", requestId, interruptors: [] }) ) }) ) - const sendRequestDefect = (client: Client, requestId: RequestId, defect: unknown) => + const encodeDefect = Schema.encodeSync(Schema.Defect) + + const sendRequestDefect = ( + client: Client, + requestId: RequestId, + encodeDefect: (u: unknown) => Effect.Effect, + defect: unknown + ) => Effect.catchAllCause( - send(client.id, { - _tag: "Exit", - requestId: String(requestId), - exit: { - _tag: "Failure", - cause: { - _tag: "Die", - defect + encodeDefect(defect).pipe(Effect.flatMap((encodedDefect) => + send(client.id, { + _tag: "Exit", + requestId: String(requestId), + exit: { + _tag: "Failure", + cause: { + _tag: "Die", + defect: encodedDefect + } } - } - }), + }) + )), (cause) => sendDefect(client, Cause.squash(cause)) ) const sendDefect = (client: Client, defect: unknown) => Effect.catchAllCause( - send(client.id, { _tag: "Defect", defect }), + send(client.id, { _tag: "Defect", defect: encodeDefect(defect) }), (cause) => Effect.annotateLogs(Effect.logDebug(cause), { module: "RpcServer", @@ -659,7 +673,8 @@ export const make: ( return Effect.matchEffect( Effect.provide(schemas.decode(request.payload), schemas.context), { - onFailure: (error) => sendRequestDefect(client, requestId, TreeFormatter.formatErrorSync(error)), + onFailure: (error) => + sendRequestDefect(client, requestId, schemas.encodeDefect, TreeFormatter.formatErrorSync(error)), onSuccess: (payload) => { client.schemas.set( requestId, @@ -1127,7 +1142,7 @@ export const makeProtocolWorkerRunner: Effect.Effect< yield* Deferred.await(closeLatch).pipe( Effect.onExit(() => { - fiber.currentScheduler.scheduleTask(() => fiber.unsafeInterruptAsFork(fiber.id()), 0) + fiber.currentScheduler.scheduleTask(() => fiber.unsafeInterruptAsFork(fiber.id()), 0, fiber) return Effect.void }), Effect.forkScoped diff --git a/packages/rpc/test/Rpc.test.ts b/packages/rpc/test/Rpc.test.ts index ab747dc678f..2d3cb3067fd 100644 --- a/packages/rpc/test/Rpc.test.ts +++ b/packages/rpc/test/Rpc.test.ts @@ -1,7 +1,7 @@ import { Headers } from "@effect/platform" import { Rpc, RpcGroup } from "@effect/rpc" import { assert, describe, it } from "@effect/vitest" -import { Effect, Schema } from "effect" +import { Cause, Effect, Exit, Schema } from "effect" const TestGroup = RpcGroup.make( Rpc.make("one"), @@ -20,4 +20,25 @@ describe("Rpc", () => { const result = yield* handler(void 0, Headers.empty) assert.strictEqual(result, "two") })) + + it("exitSchema uses custom defect schema", () => { + const myRpc = Rpc.make("customDefect", { + success: Schema.String, + defect: Schema.Unknown + }) + + const schema = Rpc.exitSchema(myRpc) + const encode = Schema.encodeSync(schema) + const decode = Schema.decodeSync(schema) + + const error = { message: "boom", stack: "Error: boom\n at foo.ts:1", code: 42 } + const exit = Exit.die(error) + + // Schema.Unknown preserves the full defect value, unlike the default Schema.Defect + const roundTripped = decode(encode(exit)) + + assert.isTrue(Exit.isFailure(roundTripped)) + const defect = Cause.squash((roundTripped as Exit.Failure).cause) + assert.deepStrictEqual(defect, error) + }) }) diff --git a/packages/rpc/test/RpcSerialization.test.ts b/packages/rpc/test/RpcSerialization.test.ts new file mode 100644 index 00000000000..574c12998c2 --- /dev/null +++ b/packages/rpc/test/RpcSerialization.test.ts @@ -0,0 +1,61 @@ +import { RpcSerialization } from "@effect/rpc" +import { assert, describe, it } from "@effect/vitest" + +describe("RpcSerialization", () => { + describe("msgPack", () => { + it("encode and decode correctly", () => { + const parser = RpcSerialization.msgPack.unsafeMake() + const payload = { _tag: "Request", id: 1, method: "echo" } + const encoded = parser.encode(payload) + const decoded = parser.decode(encoded as Uint8Array) + assert.strictEqual(decoded.length, 1) + assert.deepStrictEqual(decoded[0], payload) + }) + + it("handles incomplete frames gracefully", () => { + const parser = RpcSerialization.msgPack.unsafeMake() + const helper = RpcSerialization.msgPack.unsafeMake() + + const msg1 = helper.encode({ a: 1 }) as Uint8Array + const msg2 = helper.encode({ b: 2 }) as Uint8Array + const combined = new Uint8Array(msg1.length + msg2.length) + combined.set(msg1) + combined.set(msg2, msg1.length) + + const truncated = combined.subarray(0, msg1.length + 2) + const decoded = parser.decode(truncated) + + assert.strictEqual(decoded.length, 1) + assert.deepStrictEqual(decoded[0], { a: 1 }) + }) + }) + + describe("makeMsgPack", () => { + it("useRecords false encode and decode correctly", () => { + const parser = RpcSerialization.makeMsgPack({ useRecords: false }).unsafeMake() + const payload = { _tag: "Request", id: 1, method: "echo" } + const encoded = parser.encode(payload) + const decoded = parser.decode(encoded as Uint8Array) + assert.strictEqual(decoded.length, 1) + assert.deepStrictEqual(decoded[0], payload) + }) + + it("useRecords false handles nested objects with repeated structures", () => { + const parser = RpcSerialization.makeMsgPack({ useRecords: false }).unsafeMake() + const payload = { + _tag: "Chunk", + requestId: "1", + values: [ + { _tag: "Exit", requestId: "1", exit: { _tag: "Success", value: { _tag: "Ok", data: "a" } } }, + { _tag: "Exit", requestId: "2", exit: { _tag: "Success", value: { _tag: "Ok", data: "b" } } }, + { _tag: "Exit", requestId: "3", exit: { _tag: "Success", value: { _tag: "Ok", data: "c" } } }, + { _tag: "Exit", requestId: "4", exit: { _tag: "Success", value: { _tag: "Ok", data: "d" } } } + ] + } + const encoded = parser.encode(payload) + const decoded = parser.decode(encoded as Uint8Array) + assert.strictEqual(decoded.length, 1) + assert.deepStrictEqual(decoded[0], payload) + }) + }) +}) diff --git a/packages/sql-clickhouse/CHANGELOG.md b/packages/sql-clickhouse/CHANGELOG.md index 5de867f1482..dc4e477a49b 100644 --- a/packages/sql-clickhouse/CHANGELOG.md +++ b/packages/sql-clickhouse/CHANGELOG.md @@ -1,5 +1,27 @@ # @effect/sql-clickhouse +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/platform-node@0.106.0 + - @effect/sql@0.51.0 + +## 0.47.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/platform-node@0.105.0 + - @effect/sql@0.50.0 + ## 0.46.0 ### Patch Changes diff --git a/packages/sql-clickhouse/package.json b/packages/sql-clickhouse/package.json index 3d41352c7ba..2dc2c17c033 100644 --- a/packages/sql-clickhouse/package.json +++ b/packages/sql-clickhouse/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-clickhouse", - "version": "0.46.0", + "version": "0.48.0", "type": "module", "license": "MIT", "description": "A Clickhouse toolkit for Effect", diff --git a/packages/sql-d1/CHANGELOG.md b/packages/sql-d1/CHANGELOG.md index 3c2d0e6a564..f79e14bee8d 100644 --- a/packages/sql-d1/CHANGELOG.md +++ b/packages/sql-d1/CHANGELOG.md @@ -1,5 +1,25 @@ # @effect/sql-d1 +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/sql@0.51.0 + +## 0.48.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/sql@0.50.0 + ## 0.47.0 ### Patch Changes diff --git a/packages/sql-d1/package.json b/packages/sql-d1/package.json index e607af5fc6e..0a8ea373f69 100644 --- a/packages/sql-d1/package.json +++ b/packages/sql-d1/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-d1", - "version": "0.47.0", + "version": "0.49.0", "type": "module", "license": "MIT", "description": "A Cloudflare D1 integration for Effect", diff --git a/packages/sql-drizzle/CHANGELOG.md b/packages/sql-drizzle/CHANGELOG.md index b0755c06eff..a46df9faaa5 100644 --- a/packages/sql-drizzle/CHANGELOG.md +++ b/packages/sql-drizzle/CHANGELOG.md @@ -1,5 +1,27 @@ # @effect/sql-drizzle +## 0.50.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/sql@0.51.0 + +## 0.49.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/sql@0.50.0 + +## 0.48.1 + +### Patch Changes + +- [#6033](https://github.com/Effect-TS/effect/pull/6033) [`740a912`](https://github.com/Effect-TS/effect/commit/740a912142c2578defcf3e1e7d449535b074bd61) Thanks @tim-smart! - Add `PgClient.fromPool` and `PgClient.layerFromPool`. + ## 0.48.0 ### Patch Changes diff --git a/packages/sql-drizzle/package.json b/packages/sql-drizzle/package.json index cf966fda3dd..4c0021d7e6b 100644 --- a/packages/sql-drizzle/package.json +++ b/packages/sql-drizzle/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-drizzle", - "version": "0.48.0", + "version": "0.50.0", "type": "module", "license": "MIT", "description": "Drizzle integration for @effect/sql", @@ -59,8 +59,10 @@ "@effect/sql-sqlite-node": "workspace:^", "@testcontainers/mysql": "^10.25.0", "@testcontainers/postgresql": "^10.25.0", + "@types/pg": "^8.15.6", "drizzle-orm": "^0.43.1", - "effect": "workspace:^" + "effect": "workspace:^", + "pg": "^8.16.3" }, "peerDependencies": { "@effect/sql": "workspace:^", diff --git a/packages/sql-kysely/CHANGELOG.md b/packages/sql-kysely/CHANGELOG.md index d2b19881fea..3e03ab9be9b 100644 --- a/packages/sql-kysely/CHANGELOG.md +++ b/packages/sql-kysely/CHANGELOG.md @@ -1,5 +1,30 @@ # @effect/sql-kysely +## 0.47.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/sql@0.51.0 + +## 0.46.1 + +### Patch Changes + +- [#6123](https://github.com/Effect-TS/effect/pull/6123) [`041dd22`](https://github.com/Effect-TS/effect/commit/041dd22de07235de08b75f0bc29e206e3c27942b) Thanks @0xh3x! - Fix proxy get invariant violation in sql-kysely when hashing cached properties + +- Updated dependencies [[`add06f4`](https://github.com/Effect-TS/effect/commit/add06f4521403cbf4b9a692f9b59fb9d3d48293c), [`a03b6a2`](https://github.com/Effect-TS/effect/commit/a03b6a29ed0b983b0440b8ef4be47f47c57d73d7)]: + - effect@3.20.1 + +## 0.46.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/sql@0.50.0 + ## 0.45.0 ### Patch Changes diff --git a/packages/sql-kysely/package.json b/packages/sql-kysely/package.json index 8b63a3060e3..a9665008d52 100644 --- a/packages/sql-kysely/package.json +++ b/packages/sql-kysely/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-kysely", - "version": "0.45.0", + "version": "0.47.0", "type": "module", "license": "MIT", "description": "Kysely integration for @effect/sql", @@ -60,7 +60,7 @@ "@testcontainers/mysql": "^10.25.0", "@testcontainers/postgresql": "^10.25.0", "@types/better-sqlite3": "^7.6.13", - "better-sqlite3": "^11.10.0", + "better-sqlite3": "^12.6.2", "effect": "workspace:^", "kysely": "^0.28.2" }, diff --git a/packages/sql-kysely/src/internal/patch.ts b/packages/sql-kysely/src/internal/patch.ts index 522c831ec95..fb4b0ddcac3 100644 --- a/packages/sql-kysely/src/internal/patch.ts +++ b/packages/sql-kysely/src/internal/patch.ts @@ -41,6 +41,12 @@ function effectifyWith( } return new Proxy(obj, { get(target, prop): any { + // Respect the proxy invariant: non-configurable, non-writable + // properties must return their actual value. + const desc = Object.getOwnPropertyDescriptor(target, prop) + if (desc && !desc.configurable && !desc.writable) { + return target[prop] + } const prototype = Object.getPrototypeOf(target) if (Effect.EffectTypeId in prototype && prop === "commit") { return commit.bind(target) diff --git a/packages/sql-libsql/CHANGELOG.md b/packages/sql-libsql/CHANGELOG.md index f1550e09722..ef6c5882384 100644 --- a/packages/sql-libsql/CHANGELOG.md +++ b/packages/sql-libsql/CHANGELOG.md @@ -1,5 +1,25 @@ # @effect/sql-libsql +## 0.41.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/sql@0.51.0 + +## 0.40.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/sql@0.50.0 + ## 0.39.0 ### Patch Changes diff --git a/packages/sql-libsql/package.json b/packages/sql-libsql/package.json index 76c31f376e5..6d4c25204fe 100644 --- a/packages/sql-libsql/package.json +++ b/packages/sql-libsql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-libsql", - "version": "0.39.0", + "version": "0.41.0", "type": "module", "license": "MIT", "description": "A libSQL toolkit for Effect", diff --git a/packages/sql-mssql/CHANGELOG.md b/packages/sql-mssql/CHANGELOG.md index 75593c84570..863245e8319 100644 --- a/packages/sql-mssql/CHANGELOG.md +++ b/packages/sql-mssql/CHANGELOG.md @@ -1,5 +1,25 @@ # @effect/sql-mssql +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/sql@0.51.0 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/sql@0.50.0 + ## 0.50.0 ### Patch Changes diff --git a/packages/sql-mssql/package.json b/packages/sql-mssql/package.json index c9cca0d6787..ab66be5b314 100644 --- a/packages/sql-mssql/package.json +++ b/packages/sql-mssql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mssql", - "version": "0.50.0", + "version": "0.52.0", "type": "module", "license": "MIT", "description": "A Microsoft SQL Server toolkit for Effect", diff --git a/packages/sql-mysql2/CHANGELOG.md b/packages/sql-mysql2/CHANGELOG.md index 534100647b5..bdfa1d3dd5f 100644 --- a/packages/sql-mysql2/CHANGELOG.md +++ b/packages/sql-mysql2/CHANGELOG.md @@ -1,5 +1,25 @@ # @effect/sql-mysql2 +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/sql@0.51.0 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/sql@0.50.0 + ## 0.50.0 ### Patch Changes diff --git a/packages/sql-mysql2/package.json b/packages/sql-mysql2/package.json index d7bec78a289..950ed9934e1 100644 --- a/packages/sql-mysql2/package.json +++ b/packages/sql-mysql2/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mysql2", - "version": "0.50.0", + "version": "0.52.0", "type": "module", "license": "MIT", "description": "A MySQL toolkit for Effect", diff --git a/packages/sql-pg/CHANGELOG.md b/packages/sql-pg/CHANGELOG.md index 322d6d214ef..704e3b7f153 100644 --- a/packages/sql-pg/CHANGELOG.md +++ b/packages/sql-pg/CHANGELOG.md @@ -1,5 +1,50 @@ # @effect/sql-pg +## 0.52.1 + +### Patch Changes + +- [#6140](https://github.com/Effect-TS/effect/pull/6140) [`4767f86`](https://github.com/Effect-TS/effect/commit/4767f860a8231fc8d805d38173b85b47da41a6b0) Thanks @tim-smart! - Use a dedicated PostgreSQL connection for LISTEN / UNLISTEN so active listeners do not consume a pooled query connection. + +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/sql@0.51.0 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/sql@0.50.0 + +## 0.50.3 + +### Patch Changes + +- [#6033](https://github.com/Effect-TS/effect/pull/6033) [`740a912`](https://github.com/Effect-TS/effect/commit/740a912142c2578defcf3e1e7d449535b074bd61) Thanks @tim-smart! - Add `PgClient.fromPool` and `PgClient.layerFromPool`. + +- Updated dependencies [[`22d9d27`](https://github.com/Effect-TS/effect/commit/22d9d27bc007db86d9e4748c17324fab5f950c7d)]: + - @effect/platform@0.94.4 + +## 0.50.2 + +### Patch Changes + +- [#5998](https://github.com/Effect-TS/effect/pull/5998) [`7b8165f`](https://github.com/Effect-TS/effect/commit/7b8165f45779380fea8ac8e09badef898b63eb41) Thanks @Brandon-Perry! - Readded stream as an optional parameter to PgClientConfig. + +- Updated dependencies [[`0023c19`](https://github.com/Effect-TS/effect/commit/0023c19c63c402c050d496817ba92aceea7f25b7), [`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49), [`9a96b87`](https://github.com/Effect-TS/effect/commit/9a96b87a33a75ebc277c585e60758ab4409c0d9e)]: + - @effect/platform@0.94.3 + - effect@3.19.16 + ## 0.50.1 ### Patch Changes diff --git a/packages/sql-pg/package.json b/packages/sql-pg/package.json index 31ad2aa87b0..f526348defa 100644 --- a/packages/sql-pg/package.json +++ b/packages/sql-pg/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-pg", - "version": "0.50.1", + "version": "0.52.1", "type": "module", "license": "MIT", "description": "A PostgreSQL toolkit for Effect", diff --git a/packages/sql-pg/src/PgClient.ts b/packages/sql-pg/src/PgClient.ts index 500a70a253e..aaf37d87e17 100644 --- a/packages/sql-pg/src/PgClient.ts +++ b/packages/sql-pg/src/PgClient.ts @@ -96,12 +96,17 @@ export interface PgClientConfig { readonly types?: Pg.CustomTypesConfig | undefined } -/** - * @category constructors - * @since 1.0.0 - */ -export const make = ( - options: PgClientConfig +type ClientOptions = { + readonly spanAttributes?: Record | undefined + readonly transformResultNames?: ((str: string) => string) | undefined + readonly transformQueryNames?: ((str: string) => string) | undefined + readonly transformJson?: boolean | undefined +} + +const makeClient = ( + pool: Pg.Pool, + config: PgClientConfig, + options: ClientOptions ): Effect.Effect => Effect.gen(function*() { const compiler = makeCompiler( @@ -115,54 +120,6 @@ export const make = ( ).array : undefined - const pool = new Pg.Pool({ - connectionString: options.url ? Redacted.value(options.url) : undefined, - user: options.username, - host: options.host, - database: options.database, - password: options.password ? Redacted.value(options.password) : undefined, - ssl: options.ssl, - port: options.port, - stream: options.stream!, - connectionTimeoutMillis: options.connectTimeout - ? Duration.toMillis(options.connectTimeout) - : undefined, - idleTimeoutMillis: options.idleTimeout - ? Duration.toMillis(options.idleTimeout) - : undefined, - max: options.maxConnections, - min: options.minConnections, - maxLifetimeSeconds: options.connectionTTL - ? Duration.toSeconds(options.connectionTTL) - : undefined, - application_name: options.applicationName ?? "@effect/sql-pg", - types: options.types - }) - - pool.on("error", (_err) => { - }) - - yield* Effect.acquireRelease( - Effect.tryPromise({ - try: () => pool.query("SELECT 1"), - catch: (cause) => new SqlError({ cause, message: "PgClient: Failed to connect" }) - }), - () => - Effect.promise(() => pool.end()).pipe( - Effect.interruptible, - Effect.timeoutOption(1000) - ) - ).pipe( - Effect.timeoutFail({ - duration: options.connectTimeout ?? Duration.seconds(5), - onTimeout: () => - new SqlError({ - cause: new Error("Connection timed out"), - message: "PgClient: Connection timed out" - }) - }) - ) - class ConnectionImpl implements Connection { readonly pg: Pg.PoolClient | undefined constructor(pg?: Pg.PoolClient) { @@ -354,27 +311,35 @@ export const make = ( }) const reserve = Effect.map(reserveRaw, (client) => new ConnectionImpl(client)) + const onListenClientError = (_: Error) => { + } + const listenClient = yield* RcRef.make({ - acquire: reserveRaw + acquire: Effect.acquireRelease( + Effect.tryPromise({ + try: async () => { + const client = new Pg.Client(pool.options) + await client.connect() + client.on("error", onListenClientError) + return client + }, + catch: (cause) => + new SqlError({ + cause, + message: "Failed to acquire connection for listen" + }) + }), + (client) => + Effect.promise(() => { + client.off("error", onListenClientError) + return client.end() + }).pipe( + Effect.interruptible, + Effect.timeoutOption(1000) + ) + ) }) - let config = options - if (pool.options.connectionString) { - try { - const parsed = PgConnString.parse(pool.options.connectionString) - config = { - ...config, - host: config.host ?? parsed.host ?? undefined, - port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined), - username: config.username ?? parsed.user ?? undefined, - password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined), - database: config.database ?? parsed.database ?? undefined - } - } catch { - // - } - } - return Object.assign( yield* Client.make({ acquirer: Effect.succeed(new ConnectionImpl()), @@ -383,9 +348,9 @@ export const make = ( spanAttributes: [ ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), [ATTR_DB_SYSTEM_NAME, "postgresql"], - [ATTR_DB_NAMESPACE, options.database ?? options.username ?? "postgres"], - [ATTR_SERVER_ADDRESS, options.host ?? "localhost"], - [ATTR_SERVER_PORT, options.port ?? 5432] + [ATTR_DB_NAMESPACE, config.database ?? config.username ?? "postgres"], + [ATTR_SERVER_ADDRESS, config.host ?? "localhost"], + [ATTR_SERVER_PORT, config.port ?? 5432] ], transformRows }), @@ -427,6 +392,142 @@ export const make = ( ) }) +/** + * @category constructors + * @since 1.0.0 + */ +export const make = ( + options: PgClientConfig +): Effect.Effect => + Effect.gen(function*() { + const pool = new Pg.Pool({ + connectionString: options.url ? Redacted.value(options.url) : undefined, + user: options.username, + host: options.host, + database: options.database, + password: options.password ? Redacted.value(options.password) : undefined, + ssl: options.ssl, + port: options.port, + ...(options.stream ? { stream: options.stream } : {}), + connectionTimeoutMillis: options.connectTimeout + ? Duration.toMillis(options.connectTimeout) + : undefined, + idleTimeoutMillis: options.idleTimeout + ? Duration.toMillis(options.idleTimeout) + : undefined, + max: options.maxConnections, + min: options.minConnections, + maxLifetimeSeconds: options.connectionTTL + ? Duration.toSeconds(options.connectionTTL) + : undefined, + application_name: options.applicationName ?? "@effect/sql-pg", + types: options.types + }) + + pool.on("error", (_err) => { + }) + + yield* Effect.acquireRelease( + Effect.tryPromise({ + try: () => pool.query("SELECT 1"), + catch: (cause) => new SqlError({ cause, message: "PgClient: Failed to connect" }) + }), + () => + Effect.promise(() => pool.end()).pipe( + Effect.interruptible, + Effect.timeoutOption(1000) + ) + ).pipe( + Effect.timeoutFail({ + duration: options.connectTimeout ?? Duration.seconds(5), + onTimeout: () => + new SqlError({ + cause: new Error("Connection timed out"), + message: "PgClient: Connection timed out" + }) + }) + ) + + let config = options + if (pool.options.connectionString) { + try { + const parsed = PgConnString.parse(pool.options.connectionString) + config = { + ...config, + host: config.host ?? parsed.host ?? undefined, + port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined), + username: config.username ?? parsed.user ?? undefined, + password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined), + database: config.database ?? parsed.database ?? undefined + } + } catch { + // + } + } + + return yield* makeClient(pool, config, options) + }) + +/** + * @category constructors + * @since 1.0.0 + */ +export interface PgClientFromPoolOptions { + readonly acquire: Effect.Effect + + readonly applicationName?: string | undefined + readonly spanAttributes?: Record | undefined + + readonly transformResultNames?: ((str: string) => string) | undefined + readonly transformQueryNames?: ((str: string) => string) | undefined + readonly transformJson?: boolean | undefined + readonly types?: Pg.CustomTypesConfig | undefined +} + +/** + * Create a `PgClient` from an existing `pg` pool. + * + * You control the pool lifecycle via `acquire` (typically `Effect.acquireRelease`). + * + * @category constructors + * @since 1.0.0 + */ +export const fromPool = Effect.fnUntraced(function*( + options: PgClientFromPoolOptions +): Effect.fn.Return { + const pool = yield* options.acquire + + let config: PgClientConfig = { + url: pool.options.connectionString ? Redacted.make(pool.options.connectionString) : undefined, + host: pool.options.host, + port: pool.options.port, + database: pool.options.database, + username: pool.options.user, + password: typeof pool.options.password === "string" ? Redacted.make(pool.options.password) : undefined, + ssl: pool.options.ssl, + applicationName: (pool.options as any).application_name, + types: pool.options.types + } + + if (pool.options.connectionString) { + try { + const parsed = PgConnString.parse(pool.options.connectionString) + config = { + ...config, + host: config.host ?? parsed.host ?? undefined, + port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined), + username: config.username ?? parsed.user ?? undefined, + password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined), + database: config.database ?? parsed.database ?? undefined + } + } catch { + // + } + } + + return yield* makeClient(pool, config, options) +}) + const cancelEffects = new WeakMap | undefined>() const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => { if (cancelEffects.has(client)) { @@ -481,6 +582,20 @@ export const layer = ( )) ).pipe(Layer.provide(Reactivity.layer)) +/** + * @category layers + * @since 1.0.0 + */ +export const layerFromPool = ( + options: PgClientFromPoolOptions +): Layer.Layer => + Layer.scopedContext( + Effect.map(fromPool(options), (client) => + Context.make(PgClient, client).pipe( + Context.add(Client.SqlClient, client) + )) + ).pipe(Layer.provide(Reactivity.layer)) + /** * @category constructor * @since 1.0.0 diff --git a/packages/sql-pg/test/Client.test.ts b/packages/sql-pg/test/Client.test.ts index 230b35bd8e6..2925051a4da 100644 --- a/packages/sql-pg/test/Client.test.ts +++ b/packages/sql-pg/test/Client.test.ts @@ -2,7 +2,7 @@ import { PgClient } from "@effect/sql-pg" import * as SqlClient from "@effect/sql/SqlClient" import * as Statement from "@effect/sql/Statement" import { assert, expect, it } from "@effect/vitest" -import { Effect, Redacted, String } from "effect" +import { Effect, Fiber, Redacted, String } from "effect" import * as Chunk from "effect/Chunk" import * as Stream from "effect/Stream" import * as TestServices from "effect/TestServices" @@ -318,3 +318,63 @@ it.layer(PgContainer.ClientTransformLive, { timeout: "30 seconds" })("PgClient t expect(sql.config.database).toEqual(parsedConfig.database) })) }) + +it.layer(PgContainer.ClientFromPoolLive, { timeout: "30 seconds" })("PgClient fromPool", (it) => { + it.effect("uses compiler transforms", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const [query] = sql`SELECT * from ${sql("peopleTest")}`.compile() + expect(query).toEqual(`SELECT * from "people_test"`) + })) + + it.effect("Should populate config", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + + assert.isDefined(sql.config.url) + const parsedConfig = parsePgConnectionString(Redacted.value(sql.config.url)) + + expect(sql.config.host).toEqual(parsedConfig.host) + assert.isNotNull(parsedConfig.port) + assert.isDefined(parsedConfig.port) + expect(sql.config.port).toEqual(parseInt(parsedConfig.port)) + expect(sql.config.username).toEqual(parsedConfig.user) + assert.isDefined(sql.config.password) + expect(Redacted.value(sql.config.password)).toEqual(parsedConfig.password) + expect(sql.config.database).toEqual(parsedConfig.database) + })) +}) + +it.layer(PgContainer.ClientSingleConnectionLive, { timeout: "30 seconds" })("PgClient listen", (it) => { + it.scoped("listen does not reserve a pool connection", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const channel = "pool_connection_listen" + + const listenFiber = yield* sql.listen(channel).pipe( + Stream.take(1), + Stream.runCollect, + Effect.forkScoped + ) + + yield* Effect.sleep("250 millis") + + const rows = yield* sql<{ value: number }>`SELECT 1 as value`.pipe( + Effect.timeoutFail({ + duration: "3 seconds", + onTimeout: () => new Error("query timed out while listener was active") + }) + ) + expect(rows).toEqual([{ value: 1 }]) + + yield* sql`SELECT pg_notify(${channel}, ${"payload"})` + + const payloads = yield* Fiber.join(listenFiber).pipe( + Effect.timeoutFail({ + duration: "3 seconds", + onTimeout: () => new Error("listener did not receive notification in time") + }) + ) + expect(Chunk.toReadonlyArray(payloads)).toEqual(["payload"]) + }).pipe(TestServices.provideLive)) +}) diff --git a/packages/sql-pg/test/utils.ts b/packages/sql-pg/test/utils.ts index 990f6dda20f..3a0fc621603 100644 --- a/packages/sql-pg/test/utils.ts +++ b/packages/sql-pg/test/utils.ts @@ -1,6 +1,7 @@ import { PgClient } from "@effect/sql-pg" import { PostgreSqlContainer } from "@testcontainers/postgresql" import { Data, Effect, Layer, Redacted, String } from "effect" +import * as Pg from "pg" export class ContainerError extends Data.TaggedError("ContainerError")<{ cause: unknown @@ -34,4 +35,29 @@ export class PgContainer extends Effect.Service()("test/PgContainer }) }) ).pipe(Layer.provide(this.Default)) + + static ClientFromPoolLive = Layer.unwrapEffect( + Effect.gen(function*() { + const container = yield* PgContainer + const acquire = Effect.acquireRelease( + Effect.sync(() => new Pg.Pool({ connectionString: container.getConnectionUri() })), + (pool) => Effect.promise(() => pool.end()) + ) + return PgClient.layerFromPool({ + acquire, + transformResultNames: String.snakeToCamel, + transformQueryNames: String.camelToSnake + }) + }) + ).pipe(Layer.provide(this.Default)) + + static ClientSingleConnectionLive = Layer.unwrapEffect( + Effect.gen(function*() { + const container = yield* PgContainer + return PgClient.layer({ + url: Redacted.make(container.getConnectionUri()), + maxConnections: 1 + }) + }) + ).pipe(Layer.provide(this.Default)) } diff --git a/packages/sql-sqlite-bun/CHANGELOG.md b/packages/sql-sqlite-bun/CHANGELOG.md index 31d94cbc72b..050e45bb007 100644 --- a/packages/sql-sqlite-bun/CHANGELOG.md +++ b/packages/sql-sqlite-bun/CHANGELOG.md @@ -1,5 +1,44 @@ # @effect/sql-sqlite-bun +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/sql@0.51.0 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/sql@0.50.0 + +## 0.50.2 + +### Patch Changes + +- [#6044](https://github.com/Effect-TS/effect/pull/6044) [`708b8ba`](https://github.com/Effect-TS/effect/commit/708b8ba2dc4557767f9008e0dcd58f93721a065e) Thanks @0xh3x! - Wrap `db.query()` (prepare) errors in `SqlError` so they surface as catchable failures instead of defects. + +- Updated dependencies [[`22d9d27`](https://github.com/Effect-TS/effect/commit/22d9d27bc007db86d9e4748c17324fab5f950c7d)]: + - @effect/platform@0.94.4 + +## 0.50.1 + +### Patch Changes + +- [#6016](https://github.com/Effect-TS/effect/pull/6016) [`c1aaefb`](https://github.com/Effect-TS/effect/commit/c1aaefb7ad77ca033662422cbf3f3e5494c8bc8d) Thanks @0xh3x! - feat(sql-sqlite-bun): add SafeIntegers support + +- Updated dependencies [[`0023c19`](https://github.com/Effect-TS/effect/commit/0023c19c63c402c050d496817ba92aceea7f25b7), [`e71889f`](https://github.com/Effect-TS/effect/commit/e71889f35b081d13b7da2c04d2f81d6933056b49), [`9a96b87`](https://github.com/Effect-TS/effect/commit/9a96b87a33a75ebc277c585e60758ab4409c0d9e)]: + - @effect/platform@0.94.3 + - effect@3.19.16 + ## 0.50.0 ### Patch Changes diff --git a/packages/sql-sqlite-bun/examples/Client.test.ts b/packages/sql-sqlite-bun/examples/Client.test.ts index 9ece40664bf..571371a427c 100644 --- a/packages/sql-sqlite-bun/examples/Client.test.ts +++ b/packages/sql-sqlite-bun/examples/Client.test.ts @@ -2,6 +2,7 @@ import { Reactivity } from "@effect/experimental" import { BunFileSystem } from "@effect/platform-bun" import { FileSystem } from "@effect/platform/FileSystem" import { SqliteClient } from "@effect/sql-sqlite-bun" +import * as SqlClient from "@effect/sql/SqlClient" import { describe, expect, test } from "bun:test" import { Effect, pipe } from "effect" @@ -28,4 +29,40 @@ describe("Client", () => { { id: 2, name: "world" } ]) }).pipe(Effect.scoped, Effect.runPromise)) + + test("SafeIntegers returns bigint for large integers", () => + Effect.gen(function*() { + const sql = yield* makeClient + const big = 9007199254740993n // 2^53 + 1 + + yield* sql`CREATE TABLE safe_int_test (id BIGINT)` + yield* sql`INSERT INTO safe_int_test (id) VALUES (${big})` + + const resultSafe = (yield* sql`SELECT id FROM safe_int_test`.pipe( + Effect.provideService(SqlClient.SafeIntegers, true) + ))[0]?.id + expect(typeof resultSafe).toBe("bigint") + expect(resultSafe).toBe(big) + + const resultDefault = (yield* sql`SELECT id FROM safe_int_test`)[0]?.id + expect(typeof resultDefault).not.toBe("bigint") + }).pipe(Effect.scoped, Effect.runPromise)) + + test("SafeIntegers works with values query", () => + Effect.gen(function*() { + const sql = yield* makeClient + const big = 9007199254740993n // 2^53 + 1 + + yield* sql`CREATE TABLE safe_int_values_test (id BIGINT)` + yield* sql`INSERT INTO safe_int_values_test (id) VALUES (${big})` + + const resultSafe = (yield* sql`SELECT id FROM safe_int_values_test`.values.pipe( + Effect.provideService(SqlClient.SafeIntegers, true) + ))[0]?.[0] + expect(typeof resultSafe).toBe("bigint") + expect(resultSafe).toBe(big) + + const resultDefault = (yield* sql`SELECT id FROM safe_int_values_test`.values)[0]?.[0] + expect(typeof resultDefault).not.toBe("bigint") + }).pipe(Effect.scoped, Effect.runPromise)) }) diff --git a/packages/sql-sqlite-bun/package.json b/packages/sql-sqlite-bun/package.json index c512ce749a8..0de378448e9 100644 --- a/packages/sql-sqlite-bun/package.json +++ b/packages/sql-sqlite-bun/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-bun", - "version": "0.50.0", + "version": "0.52.0", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql-sqlite-bun/src/SqliteClient.ts b/packages/sql-sqlite-bun/src/SqliteClient.ts index f23bf96606c..71367ad5418 100644 --- a/packages/sql-sqlite-bun/src/SqliteClient.ts +++ b/packages/sql-sqlite-bun/src/SqliteClient.ts @@ -102,18 +102,32 @@ export const make = ( sql: string, params: ReadonlyArray = [] ) => - Effect.try({ - try: () => (db.query(sql).all(...(params as any)) ?? []) as Array, - catch: (cause) => new SqlError({ cause, message: "Failed to execute statement" }) + Effect.withFiberRuntime, SqlError>((fiber) => { + const useSafeIntegers = Context.get(fiber.currentContext, Client.SafeIntegers) + try { + const statement = db.query(sql) + // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 + statement.safeIntegers(useSafeIntegers) + return Effect.succeed((statement.all(...(params as any)) ?? []) as Array) + } catch (cause) { + return Effect.fail(new SqlError({ cause, message: "Failed to execute statement" })) + } }) const runValues = ( sql: string, params: ReadonlyArray = [] ) => - Effect.try({ - try: () => (db.query(sql).values(...(params as any)) ?? []) as Array, - catch: (cause) => new SqlError({ cause, message: "Failed to execute statement" }) + Effect.withFiberRuntime, SqlError>((fiber) => { + const useSafeIntegers = Context.get(fiber.currentContext, Client.SafeIntegers) + try { + const statement = db.query(sql) + // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 + statement.safeIntegers(useSafeIntegers) + return Effect.succeed((statement.values(...(params as any)) ?? []) as Array) + } catch (cause) { + return Effect.fail(new SqlError({ cause, message: "Failed to execute statement" })) + } }) return identity({ diff --git a/packages/sql-sqlite-do/CHANGELOG.md b/packages/sql-sqlite-do/CHANGELOG.md index 153b807c7e8..a29b0aedbc0 100644 --- a/packages/sql-sqlite-do/CHANGELOG.md +++ b/packages/sql-sqlite-do/CHANGELOG.md @@ -1,5 +1,23 @@ # @effect/sql-sqlite-do +## 0.29.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/sql@0.51.0 + +## 0.28.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/sql@0.50.0 + ## 0.27.0 ### Patch Changes diff --git a/packages/sql-sqlite-do/package.json b/packages/sql-sqlite-do/package.json index 1e74a4c0ab9..8d512d055cc 100644 --- a/packages/sql-sqlite-do/package.json +++ b/packages/sql-sqlite-do/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-do", - "version": "0.27.0", + "version": "0.29.0", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql-sqlite-node/CHANGELOG.md b/packages/sql-sqlite-node/CHANGELOG.md index add812e1fe6..8d983edd463 100644 --- a/packages/sql-sqlite-node/CHANGELOG.md +++ b/packages/sql-sqlite-node/CHANGELOG.md @@ -1,5 +1,34 @@ # @effect/sql-sqlite-node +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/sql@0.51.0 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/sql@0.50.0 + +## 0.50.1 + +### Patch Changes + +- [#6042](https://github.com/Effect-TS/effect/pull/6042) [`8e22862`](https://github.com/Effect-TS/effect/commit/8e2286271a982b1cc34c78fca8b9f59de71fc790) Thanks @tim-smart! - Upgrade `better-sqlite3` to v12 for the Node SQLite client. + +- Updated dependencies [[`22d9d27`](https://github.com/Effect-TS/effect/commit/22d9d27bc007db86d9e4748c17324fab5f950c7d)]: + - @effect/platform@0.94.4 + ## 0.50.0 ### Patch Changes diff --git a/packages/sql-sqlite-node/package.json b/packages/sql-sqlite-node/package.json index 335c9e39252..019468c9b85 100644 --- a/packages/sql-sqlite-node/package.json +++ b/packages/sql-sqlite-node/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-node", - "version": "0.50.0", + "version": "0.52.0", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", @@ -60,6 +60,6 @@ "effect": "workspace:^" }, "dependencies": { - "better-sqlite3": "^11.10.0" + "better-sqlite3": "^12.6.2" } } diff --git a/packages/sql-sqlite-react-native/CHANGELOG.md b/packages/sql-sqlite-react-native/CHANGELOG.md index 8b48139e17b..21d128719d8 100644 --- a/packages/sql-sqlite-react-native/CHANGELOG.md +++ b/packages/sql-sqlite-react-native/CHANGELOG.md @@ -1,5 +1,23 @@ # @effect/sql-sqlite-react-native +## 0.54.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/sql@0.51.0 + +## 0.53.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/sql@0.50.0 + ## 0.52.0 ### Patch Changes diff --git a/packages/sql-sqlite-react-native/package.json b/packages/sql-sqlite-react-native/package.json index 5baba3edde7..eccadbd3bf6 100644 --- a/packages/sql-sqlite-react-native/package.json +++ b/packages/sql-sqlite-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-react-native", - "version": "0.52.0", + "version": "0.54.0", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql-sqlite-wasm/CHANGELOG.md b/packages/sql-sqlite-wasm/CHANGELOG.md index 40b0bb7400a..c19b8a36d31 100644 --- a/packages/sql-sqlite-wasm/CHANGELOG.md +++ b/packages/sql-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,23 @@ # @effect/sql-sqlite-wasm +## 0.52.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/sql@0.51.0 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/sql@0.50.0 + ## 0.50.0 ### Patch Changes diff --git a/packages/sql-sqlite-wasm/package.json b/packages/sql-sqlite-wasm/package.json index 6757576a6eb..f99916e1c2e 100644 --- a/packages/sql-sqlite-wasm/package.json +++ b/packages/sql-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-wasm", - "version": "0.50.0", + "version": "0.52.0", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql/CHANGELOG.md b/packages/sql/CHANGELOG.md index bb2ade9bb0b..0d9c707ff7f 100644 --- a/packages/sql/CHANGELOG.md +++ b/packages/sql/CHANGELOG.md @@ -1,5 +1,33 @@ # @effect/sql +## 0.51.1 + +### Patch Changes + +- [#6110](https://github.com/Effect-TS/effect/pull/6110) [`0fac630`](https://github.com/Effect-TS/effect/commit/0fac630b27095ffbfa6c48851087950ddc29cda0) Thanks @mitre88! - fix: correct typos in source code (receive, separate) + +- Updated dependencies [[`74f3267`](https://github.com/Effect-TS/effect/commit/74f3267a6cc7ed7818c4c34cc1232f7cfc7d3339), [`518d0e3`](https://github.com/Effect-TS/effect/commit/518d0e3f4879be6d9d9a7fa137a1820604bb3ea7), [`c016642`](https://github.com/Effect-TS/effect/commit/c0166426f80b7eb8e7f7d3aecc95dcd4fdb5cb55)]: + - effect@3.21.2 + - @effect/platform@0.96.1 + +## 0.51.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + +## 0.50.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + ## 0.49.0 ### Patch Changes diff --git a/packages/sql/package.json b/packages/sql/package.json index 5550bdb2a73..66d731cbee9 100644 --- a/packages/sql/package.json +++ b/packages/sql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql", - "version": "0.49.0", + "version": "0.51.1", "type": "module", "license": "MIT", "description": "A SQL toolkit for Effect", diff --git a/packages/sql/src/Statement.ts b/packages/sql/src/Statement.ts index 1858cdb7ccc..cd54bafd3e1 100644 --- a/packages/sql/src/Statement.ts +++ b/packages/sql/src/Statement.ts @@ -322,7 +322,7 @@ export interface Constructor { readonly or: (clauses: ReadonlyArray) => Fragment /** - * Create comma seperated values, with an optional prefix + * Create comma separated values, with an optional prefix * * Useful for `ORDER BY` and `GROUP BY` clauses */ diff --git a/packages/sql/tsconfig.test.json b/packages/sql/tsconfig.test.json index 55d48cec532..88df34fca95 100644 --- a/packages/sql/tsconfig.test.json +++ b/packages/sql/tsconfig.test.json @@ -7,6 +7,7 @@ ], "compilerOptions": { "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", - "rootDir": "test" + "rootDir": "test", + "outDir": "build/test" } } diff --git a/packages/typeclass/CHANGELOG.md b/packages/typeclass/CHANGELOG.md index af67be654e4..a4ec024b058 100644 --- a/packages/typeclass/CHANGELOG.md +++ b/packages/typeclass/CHANGELOG.md @@ -1,5 +1,19 @@ # @effect/typeclass +## 0.40.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + +## 0.39.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + ## 0.38.0 ### Patch Changes diff --git a/packages/typeclass/package.json b/packages/typeclass/package.json index f8effa6673d..fbd38fca788 100644 --- a/packages/typeclass/package.json +++ b/packages/typeclass/package.json @@ -1,6 +1,6 @@ { "name": "@effect/typeclass", - "version": "0.38.0", + "version": "0.40.0", "type": "module", "license": "MIT", "description": "A collection of reusable typeclasses for the Effect ecosystem", diff --git a/packages/vitest/CHANGELOG.md b/packages/vitest/CHANGELOG.md index 5964cd6a067..0d1525e048f 100644 --- a/packages/vitest/CHANGELOG.md +++ b/packages/vitest/CHANGELOG.md @@ -1,5 +1,19 @@ # @effect/vitest +## 0.29.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + +## 0.28.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + ## 0.27.0 ### Patch Changes diff --git a/packages/vitest/README.md b/packages/vitest/README.md index 9e7f06b38fc..e02212de37b 100644 --- a/packages/vitest/README.md +++ b/packages/vitest/README.md @@ -220,7 +220,7 @@ When adding new failing tests, you might not be able to fix them right away. Ins import { it } from "@effect/vitest" import { Effect, Exit } from "effect" -function divide(a: number, b: number): number { +function divide(a: number, b: number): Effect.Effect { if (b === 0) return Effect.fail("Cannot divide by zero") return Effect.succeed(a / b) } diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 9a2e5ac15fa..a4ad28d281e 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -1,6 +1,6 @@ { "name": "@effect/vitest", - "version": "0.27.0", + "version": "0.29.0", "type": "module", "license": "MIT", "description": "A set of helpers for testing Effects with vitest", diff --git a/packages/workflow/CHANGELOG.md b/packages/workflow/CHANGELOG.md index 3e82ecc0e74..c5ab1466ecb 100644 --- a/packages/workflow/CHANGELOG.md +++ b/packages/workflow/CHANGELOG.md @@ -1,5 +1,25 @@ # @effect/workflow +## 0.18.0 + +### Patch Changes + +- Updated dependencies [[`f7bb09b`](https://github.com/Effect-TS/effect/commit/f7bb09b022f195d1f2b3c23d49e74b011ec5d109), [`bd7552a`](https://github.com/Effect-TS/effect/commit/bd7552a19cc0ed575507ac6cc0879a57e24ebd31), [`ad1a7eb`](https://github.com/Effect-TS/effect/commit/ad1a7eb7f6bebaf91c80be2443ac0439226d0098), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb), [`0d32048`](https://github.com/Effect-TS/effect/commit/0d32048f9836e2b23a6ba3ec5f43f0a000bb92fb)]: + - effect@3.21.0 + - @effect/experimental@0.60.0 + - @effect/platform@0.96.0 + - @effect/rpc@0.75.0 + +## 0.17.0 + +### Patch Changes + +- Updated dependencies [[`fc82e81`](https://github.com/Effect-TS/effect/commit/fc82e81448bd9136a37580139ce46a2c61b11b54), [`82996bc`](https://github.com/Effect-TS/effect/commit/82996bce8debffcb44feb98bb862cf2662bd56b7), [`4d97a61`](https://github.com/Effect-TS/effect/commit/4d97a61a15b9dd6a0eece65b8f0c035e16d42ada), [`f6b0960`](https://github.com/Effect-TS/effect/commit/f6b0960bf3184109920dfed16ee7dfd7d67bc0f2), [`8798a84`](https://github.com/Effect-TS/effect/commit/8798a843218e6c0c0d3a8eee83360880e370b4da)]: + - effect@3.20.0 + - @effect/experimental@0.59.0 + - @effect/platform@0.95.0 + - @effect/rpc@0.74.0 + ## 0.16.0 ### Patch Changes diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 761efa87f03..9859c2e1976 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,7 +1,7 @@ { "name": "@effect/workflow", "type": "module", - "version": "0.16.0", + "version": "0.18.0", "description": "Durable workflows for Effect", "publishConfig": { "access": "public", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9457958c2b5..f6c1656bb3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,8 +53,8 @@ importers: specifier: ^0.8.3 version: 0.8.8 '@effect/docgen': - specifier: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@fd06738 - version: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@fd06738(tsx@4.20.3)(typescript@5.8.3) + specifier: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e7fe055 + version: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e7fe055(tsx@4.20.3)(typescript@5.8.3) '@effect/eslint-plugin': specifier: ^0.3.2 version: 0.3.2 @@ -87,7 +87,7 @@ importers: version: 8.37.0(eslint@9.31.0)(typescript@5.8.3) '@vitest/browser': specifier: ^3.2.4 - version: 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + version: 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) @@ -150,10 +150,10 @@ importers: version: 5.8.3 vite: specifier: ^6.1.1 - version: 6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + version: 6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) vitest: specifier: ^3.2.4 - version: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) packages/ai/ai: dependencies: @@ -377,9 +377,15 @@ importers: '@testcontainers/postgresql': specifier: ^10.25.0 version: 10.28.0 + '@types/pg': + specifier: ^8.15.6 + version: 8.15.6 effect: specifier: workspace:^ version: link:../effect + pg: + specifier: ^8.16.3 + version: 8.16.3 publishDirectory: dist packages/effect: @@ -493,8 +499,8 @@ importers: specifier: ^0.1.6 version: 0.1.6 msgpackr: - specifier: ^1.11.4 - version: 1.11.4 + specifier: ^1.11.10 + version: 1.11.10 multipasta: specifier: ^0.2.7 version: 0.2.7 @@ -665,8 +671,8 @@ importers: packages/rpc: dependencies: msgpackr: - specifier: ^1.11.4 - version: 1.11.4 + specifier: ^1.11.10 + version: 1.11.10 devDependencies: '@effect/platform': specifier: workspace:^ @@ -768,12 +774,18 @@ importers: '@testcontainers/postgresql': specifier: ^10.25.0 version: 10.28.0 + '@types/pg': + specifier: ^8.15.6 + version: 8.15.6 drizzle-orm: specifier: ^0.43.1 - version: 0.43.1(@cloudflare/workers-types@4.20250715.0)(@libsql/client@0.12.0)(@op-engineering/op-sqlite@7.1.0(react-native@0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@11.10.0)(bun-types@1.2.18(@types/react@19.1.8))(kysely@0.28.2)(mysql2@3.14.2)(pg@8.16.3)(postgres@3.4.7) + version: 0.43.1(@cloudflare/workers-types@4.20250715.0)(@libsql/client@0.12.0)(@op-engineering/op-sqlite@7.1.0(react-native@0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.6.2)(bun-types@1.2.18(@types/react@19.1.8))(kysely@0.28.2)(mysql2@3.14.2)(pg@8.16.3)(postgres@3.4.7) effect: specifier: workspace:^ version: link:../effect + pg: + specifier: ^8.16.3 + version: 8.16.3 publishDirectory: dist packages/sql-kysely: @@ -806,8 +818,8 @@ importers: specifier: ^7.6.13 version: 7.6.13 better-sqlite3: - specifier: ^11.10.0 - version: 11.10.0 + specifier: ^12.6.2 + version: 12.6.2 effect: specifier: workspace:^ version: link:../effect @@ -967,8 +979,8 @@ importers: packages/sql-sqlite-node: dependencies: better-sqlite3: - specifier: ^11.10.0 - version: 11.10.0 + specifier: ^12.6.2 + version: 12.6.2 devDependencies: '@effect/experimental': specifier: workspace:^ @@ -1000,7 +1012,7 @@ importers: version: link:../sql '@op-engineering/op-sqlite': specifier: 7.1.0 - version: 7.1.0(react-native@0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + version: 7.1.0(react-native@0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) effect: specifier: workspace:^ version: link:../effect @@ -1036,7 +1048,7 @@ importers: version: link:../effect vitest: specifier: ^3.2.4 - version: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@24.10.1)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@25.6.0)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) publishDirectory: dist packages/workflow: @@ -1265,16 +1277,24 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.0': resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.28.0': resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.5': - resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} '@babel/generator@7.12.17': @@ -1284,8 +1304,8 @@ packages: resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.5': - resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.3': @@ -1296,6 +1316,10 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.27.1': resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} engines: {node: '>=6.9.0'} @@ -1314,14 +1338,18 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.27.3': resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1334,6 +1362,10 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-replace-supers@7.27.1': resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} engines: {node: '>=6.9.0'} @@ -1364,8 +1396,8 @@ packages: resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} '@babel/parser@7.28.0': @@ -1373,8 +1405,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true @@ -1405,8 +1437,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1545,28 +1577,32 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.0': resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.5': - resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} '@babel/types@7.28.1': resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} '@balena/dockerignore@1.0.2': @@ -1704,8 +1740,8 @@ packages: engines: {node: '>=16.17.1'} hasBin: true - '@effect/docgen@https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@fd06738': - resolution: {tarball: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@fd06738} + '@effect/docgen@https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e7fe055': + resolution: {tarball: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e7fe055} version: 0.5.2 engines: {node: '>=18.0.0'} hasBin: true @@ -2104,6 +2140,10 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + '@jest/create-cache-key-function@29.7.0': resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2899,6 +2939,9 @@ packages: '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -3201,6 +3244,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -3418,6 +3466,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} + engines: {node: '>=6.0.0'} + hasBin: true + bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} @@ -3425,8 +3478,9 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - better-sqlite3@11.10.0: - resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -3447,6 +3501,9 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -3459,6 +3516,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -3538,6 +3600,9 @@ packages: caniuse-lite@1.0.30001727: resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + chai@5.2.1: resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} engines: {node: '>=18'} @@ -4049,6 +4114,9 @@ packages: electron-to-chromium@1.5.185: resolution: {integrity: sha512-dYOZfUk57hSMPePoIQ1fZWl1Fkj+OshhEVuPacNKWzC1efe56OsHY3l/jCfiAgIICOU3VgOIdoq7ahg7r7n6MQ==} + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4549,16 +4617,18 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -4650,8 +4720,8 @@ packages: htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} http-proxy-agent@7.0.2: @@ -5174,6 +5244,7 @@ packages: libsql@0.4.7: resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} + cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] lighthouse-logger@1.4.2: @@ -5450,6 +5521,9 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -5529,8 +5603,8 @@ packages: resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} hasBin: true - msgpackr@1.11.4: - resolution: {integrity: sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==} + msgpackr@1.11.10: + resolution: {integrity: sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==} multipasta@0.2.7: resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} @@ -5613,6 +5687,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + node-source-walk@6.0.2: resolution: {integrity: sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag==} engines: {node: '>=14'} @@ -5961,6 +6038,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true precinct@11.0.5: @@ -6280,8 +6358,13 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} seq-queue@0.0.5: @@ -6291,8 +6374,8 @@ packages: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} set-function-length@1.2.2: @@ -6460,8 +6543,8 @@ packages: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} std-env@3.9.0: @@ -6589,6 +6672,7 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tedious@18.6.1: resolution: {integrity: sha512-9AvErXXQTd6l7TDd5EmM+nxbOGyhnmdbp/8c3pw+tjaiSXW9usME90ET/CRG1LN1Y9tPMtz/p83z4Q97B4DDpw==} @@ -6598,8 +6682,8 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - terser@5.44.1: - resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} engines: {node: '>=10'} hasBin: true @@ -6802,6 +6886,9 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici@5.29.0: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} @@ -6834,6 +6921,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -6967,6 +7060,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} @@ -7328,8 +7422,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.0': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.28.0': dependencies: '@ampproject/remapping': 2.3.0 @@ -7350,17 +7452,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.28.5': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -7384,10 +7486,10 @@ snapshots: '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 - '@babel/generator@7.28.5': + '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -7404,6 +7506,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -7433,6 +7543,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -7442,12 +7559,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -7457,6 +7574,8 @@ snapshots: '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -7486,103 +7605,103 @@ snapshots: '@babel/template': 7.27.2 '@babel/types': 7.28.1 - '@babel/helpers@7.28.4': + '@babel/helpers@7.29.2': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@babel/parser@7.28.0': dependencies: '@babel/types': 7.28.1 - '@babel/parser@7.28.5': + '@babel/parser@7.29.2': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': dependencies: @@ -7677,7 +7796,7 @@ snapshots: '@babel/runtime@7.27.6': {} - '@babel/runtime@7.28.4': {} + '@babel/runtime@7.29.2': {} '@babel/template@7.27.2': dependencies: @@ -7685,6 +7804,12 @@ snapshots: '@babel/parser': 7.28.0 '@babel/types': 7.28.1 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@babel/traverse@7.28.0': dependencies: '@babel/code-frame': 7.27.1 @@ -7697,14 +7822,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.5': + '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -7714,7 +7839,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/types@7.28.5': + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 @@ -7927,7 +8052,7 @@ snapshots: micromatch: 4.0.8 pkg-entry-points: 1.1.1 - '@effect/docgen@https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@fd06738(tsx@4.20.3)(typescript@5.8.3)': + '@effect/docgen@https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e7fe055(tsx@4.20.3)(typescript@5.8.3)': dependencies: '@babel/code-frame': 7.27.1 '@effect/markdown-toc': 0.1.0 @@ -8235,6 +8360,8 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@istanbuljs/schema@0.1.6': {} + '@jest/create-cache-key-function@29.7.0': dependencies: '@jest/types': 29.6.3 @@ -8243,7 +8370,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.10.1 + '@types/node': 25.6.0 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -8254,7 +8381,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 24.10.1 + '@types/node': 25.6.0 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -8265,7 +8392,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 @@ -8472,10 +8599,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@op-engineering/op-sqlite@7.1.0(react-native@0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': + '@op-engineering/op-sqlite@7.1.0(react-native@0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 - react-native: 0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0) + react-native: 0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0) '@opentelemetry/api-logs@0.208.0': dependencies: @@ -8679,9 +8806,9 @@ snapshots: '@react-native/assets-registry@0.80.1': {} - '@react-native/codegen@0.80.1(@babel/core@7.28.5)': + '@react-native/codegen@0.80.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 glob: 7.2.3 hermes-parser: 0.28.1 invariant: 2.2.4 @@ -8697,7 +8824,7 @@ snapshots: metro: 0.82.5 metro-config: 0.82.5 metro-core: 0.82.5 - semver: 7.7.3 + semver: 7.7.4 transitivePeerDependencies: - bufferutil - supports-color @@ -8716,7 +8843,7 @@ snapshots: invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 - serve-static: 1.16.2 + serve-static: 1.16.3 ws: 6.2.3 transitivePeerDependencies: - bufferutil @@ -8729,12 +8856,12 @@ snapshots: '@react-native/normalize-colors@0.80.1': {} - '@react-native/virtualized-lists@0.80.1(@types/react@19.1.8)(react-native@0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': + '@react-native/virtualized-lists@0.80.1(@types/react@19.1.8)(react-native@0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.1.0 - react-native: 0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0) + react-native: 0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0) optionalDependencies: '@types/react': 19.1.8 @@ -8934,24 +9061,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/better-sqlite3@7.6.13': dependencies: @@ -9002,7 +9129,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 24.10.1 + '@types/node': 25.6.0 '@types/ini@4.1.1': {} @@ -9057,6 +9184,10 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + '@types/normalize-package-data@2.4.4': {} '@types/pg-cursor@2.7.2': @@ -9291,16 +9422,16 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/utils': 3.2.4 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) ws: 8.18.3 optionalDependencies: playwright: 1.54.1 @@ -9310,16 +9441,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/utils': 3.2.4 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@24.10.1)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@25.6.0)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) ws: 8.18.3 optionalDependencies: playwright: 1.54.1 @@ -9345,9 +9476,9 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: - '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color @@ -9359,21 +9490,21 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -9404,7 +9535,7 @@ snapshots: '@vitest/web-worker@3.2.4(vitest@3.2.4)': dependencies: debug: 4.4.1 - vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -9427,6 +9558,8 @@ snapshots: acorn@8.15.0: {} + acorn@8.16.0: {} + agent-base@7.1.4: {} ajv@6.12.6: @@ -9596,13 +9729,13 @@ snapshots: b4a@1.6.7: {} - babel-jest@29.7.0(@babel/core@7.28.5): + babel-jest@29.7.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.5) + babel-preset-jest: 29.6.3(@babel/core@7.29.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -9615,9 +9748,9 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 istanbul-lib-instrument: 5.2.1 test-exclude: 6.0.0 transitivePeerDependencies: @@ -9625,8 +9758,8 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 @@ -9634,30 +9767,30 @@ snapshots: dependencies: hermes-parser: 0.28.1 - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.5) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.5) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.5) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) - - babel-preset-jest@29.6.3(@babel/core@7.28.5): - dependencies: - '@babel/core': 7.28.5 + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@29.6.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) balanced-match@1.0.2: {} @@ -9688,6 +9821,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.20: {} + bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 @@ -9696,7 +9831,7 @@ snapshots: dependencies: is-windows: 1.0.2 - better-sqlite3@11.10.0: + better-sqlite3@12.6.2: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 @@ -9728,6 +9863,11 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -9743,6 +9883,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -9814,6 +9962,8 @@ snapshots: caniuse-lite@1.0.30001727: {} + caniuse-lite@1.0.30001788: {} + chai@5.2.1: dependencies: assertion-error: 2.0.1 @@ -9879,7 +10029,7 @@ snapshots: chrome-launcher@0.15.2: dependencies: - '@types/node': 24.10.1 + '@types/node': 25.6.0 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -9888,7 +10038,7 @@ snapshots: chromium-edge-launcher@0.2.0: dependencies: - '@types/node': 24.10.1 + '@types/node': 25.6.0 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -10247,15 +10397,15 @@ snapshots: dotenv@8.6.0: {} - drizzle-orm@0.43.1(@cloudflare/workers-types@4.20250715.0)(@libsql/client@0.12.0)(@op-engineering/op-sqlite@7.1.0(react-native@0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@11.10.0)(bun-types@1.2.18(@types/react@19.1.8))(kysely@0.28.2)(mysql2@3.14.2)(pg@8.16.3)(postgres@3.4.7): + drizzle-orm@0.43.1(@cloudflare/workers-types@4.20250715.0)(@libsql/client@0.12.0)(@op-engineering/op-sqlite@7.1.0(react-native@0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.6.2)(bun-types@1.2.18(@types/react@19.1.8))(kysely@0.28.2)(mysql2@3.14.2)(pg@8.16.3)(postgres@3.4.7): optionalDependencies: '@cloudflare/workers-types': 4.20250715.0 '@libsql/client': 0.12.0 - '@op-engineering/op-sqlite': 7.1.0(react-native@0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + '@op-engineering/op-sqlite': 7.1.0(react-native@0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.15.6 - better-sqlite3: 11.10.0 + better-sqlite3: 12.6.2 bun-types: 1.2.18(@types/react@19.1.8) kysely: 0.28.2 mysql2: 3.14.2 @@ -10278,6 +10428,8 @@ snapshots: electron-to-chromium@1.5.185: {} + electron-to-chromium@1.5.340: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -10977,7 +11129,7 @@ snapshots: gray-matter@3.1.1: dependencies: extend-shallow: 2.0.1 - js-yaml: 3.14.1 + js-yaml: 3.14.2 kind-of: 5.1.0 strip-bom-string: 1.0.0 @@ -11037,12 +11189,12 @@ snapshots: domutils: 3.2.2 entities: 6.0.1 - http-errors@2.0.0: + http-errors@2.0.1: dependencies: depd: 2.0.0 inherits: 2.0.4 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 toidentifier: 1.0.1 http-proxy-agent@7.0.2: @@ -11344,9 +11496,9 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.28.5 - '@babel/parser': 7.28.5 - '@istanbuljs/schema': 0.1.3 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 transitivePeerDependencies: @@ -11393,7 +11545,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 24.10.1 + '@types/node': 25.6.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -11403,7 +11555,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 24.10.1 + '@types/node': 25.6.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -11437,7 +11589,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.10.1 + '@types/node': 25.6.0 jest-util: 29.7.0 jest-regex-util@29.6.3: {} @@ -11462,7 +11614,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 24.10.1 + '@types/node': 25.6.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -11628,7 +11780,7 @@ snapshots: lmdb@3.4.1: dependencies: - msgpackr: 1.11.4 + msgpackr: 1.11.10 node-addon-api: 6.1.0 node-gyp-build-optional-packages: 5.2.2 ordered-binary: 1.6.0 @@ -11791,7 +11943,7 @@ snapshots: metro-babel-transformer@0.82.5: dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 flow-enums-runtime: 0.0.6 hermes-parser: 0.29.1 nullthrows: 1.1.1 @@ -11849,7 +12001,7 @@ snapshots: metro-minify-terser@0.82.5: dependencies: flow-enums-runtime: 0.0.6 - terser: 5.44.1 + terser: 5.46.1 metro-resolver@0.82.5: dependencies: @@ -11857,14 +12009,14 @@ snapshots: metro-runtime@0.82.5: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 flow-enums-runtime: 0.0.6 metro-source-map@0.82.5: dependencies: - '@babel/traverse': 7.28.5 - '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.5' - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.0 + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.29.0' + '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.82.5 @@ -11888,10 +12040,10 @@ snapshots: metro-transform-plugins@0.82.5: dependencies: - '@babel/core': 7.28.5 - '@babel/generator': 7.28.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: @@ -11899,10 +12051,10 @@ snapshots: metro-transform-worker@0.82.5: dependencies: - '@babel/core': 7.28.5 - '@babel/generator': 7.28.5 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 metro: 0.82.5 metro-babel-transformer: 0.82.5 @@ -11919,13 +12071,13 @@ snapshots: metro@0.82.5: dependencies: - '@babel/code-frame': 7.27.1 - '@babel/core': 7.28.5 - '@babel/generator': 7.28.5 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -12016,6 +12168,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.2 @@ -12086,7 +12242,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 optional: true - msgpackr@1.11.4: + msgpackr@1.11.10: optionalDependencies: msgpackr-extract: 3.0.3 @@ -12129,7 +12285,7 @@ snapshots: node-abi@3.75.0: dependencies: - semver: 7.7.2 + semver: 7.7.3 node-addon-api@6.1.0: {} @@ -12155,6 +12311,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.37: {} + node-source-walk@6.0.2: dependencies: '@babel/parser': 7.28.0 @@ -12509,7 +12667,7 @@ snapshots: pump: 3.0.3 rc: 1.2.8 simple-get: 4.0.1 - tar-fs: 2.1.3 + tar-fs: 2.1.4 tunnel-agent: 0.6.0 precinct@11.0.5: @@ -12651,20 +12809,20 @@ snapshots: react-is@18.3.1: {} - react-native@0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0): + react-native@0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.80.1 - '@react-native/codegen': 0.80.1(@babel/core@7.28.5) + '@react-native/codegen': 0.80.1(@babel/core@7.29.0) '@react-native/community-cli-plugin': 0.80.1 '@react-native/gradle-plugin': 0.80.1 '@react-native/js-polyfills': 0.80.1 '@react-native/normalize-colors': 0.80.1 - '@react-native/virtualized-lists': 0.80.1(@types/react@19.1.8)(react-native@0.80.1(@babel/core@7.28.5)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + '@react-native/virtualized-lists': 0.80.1(@types/react@19.1.8)(react-native@0.80.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 - babel-jest: 29.7.0(@babel/core@7.28.5) + babel-jest: 29.7.0(@babel/core@7.29.0) babel-plugin-syntax-hermes-parser: 0.28.1 base64-js: 1.5.1 chalk: 4.1.2 @@ -12684,7 +12842,7 @@ snapshots: react-refresh: 0.14.2 regenerator-runtime: 0.13.11 scheduler: 0.26.0 - semver: 7.7.3 + semver: 7.7.4 stacktrace-parser: 0.1.11 whatwg-fetch: 3.6.20 ws: 6.2.3 @@ -12918,21 +13076,23 @@ snapshots: semver@7.7.3: {} - send@0.19.0: + semver@7.7.4: {} + + send@0.19.2: dependencies: debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 0.5.2 - http-errors: 2.0.0 + http-errors: 2.0.1 mime: 1.6.0 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -12940,12 +13100,12 @@ snapshots: serialize-error@2.1.0: {} - serve-static@1.16.2: + serve-static@1.16.3: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.0 + send: 0.19.2 transitivePeerDependencies: - supports-color @@ -13142,7 +13302,7 @@ snapshots: statuses@1.5.0: {} - statuses@2.0.1: {} + statuses@2.0.2: {} std-env@3.9.0: {} @@ -13327,18 +13487,18 @@ snapshots: term-size@2.2.1: {} - terser@5.44.1: + terser@5.46.1: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 test-exclude@6.0.0: dependencies: - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.5 test-exclude@7.0.1: dependencies: @@ -13558,6 +13718,8 @@ snapshots: undici-types@7.16.0: {} + undici-types@7.19.2: {} + undici@5.29.0: dependencies: '@fastify/busboy': 2.1.1 @@ -13604,6 +13766,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -13623,13 +13791,13 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-node@3.2.4(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vite-node@3.2.4(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -13644,13 +13812,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vite-node@3.2.4(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -13665,7 +13833,7 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vite@6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.6 fdir: 6.4.6(picomatch@4.0.3) @@ -13676,11 +13844,11 @@ snapshots: optionalDependencies: '@types/node': 22.16.4 fsevents: 2.3.3 - terser: 5.44.1 + terser: 5.46.1 tsx: 4.20.3 yaml: 2.8.0 - vite@6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vite@6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.6 fdir: 6.4.6(picomatch@4.0.3) @@ -13689,9 +13857,9 @@ snapshots: rollup: 4.45.1 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 25.6.0 fsevents: 2.3.3 - terser: 5.44.1 + terser: 5.46.1 tsx: 4.20.3 yaml: 2.8.0 @@ -13699,13 +13867,13 @@ snapshots: dependencies: '@vitest/utils': 3.2.4 mock-socket: 9.3.1 - vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@24.10.1)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@edge-runtime/vm@5.0.0)(@types/node@25.6.0)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) - vitest@3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vitest@3.2.4(@edge-runtime/vm@5.0.0)(@types/node@22.16.4)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -13723,13 +13891,13 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@edge-runtime/vm': 5.0.0 '@types/node': 22.16.4 - '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@22.16.4)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@22.16.4)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) happy-dom: 17.6.3 transitivePeerDependencies: - jiti @@ -13745,11 +13913,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@edge-runtime/vm@5.0.0)(@types/node@24.10.1)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0): + vitest@3.2.4(@edge-runtime/vm@5.0.0)(@types/node@25.6.0)(@vitest/browser@3.2.4)(happy-dom@17.6.3)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -13767,13 +13935,13 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0) + vite: 6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@edge-runtime/vm': 5.0.0 - '@types/node': 24.10.1 - '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.10.1)(terser@5.44.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@types/node': 25.6.0 + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@25.6.0)(terser@5.46.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) happy-dom: 17.6.3 transitivePeerDependencies: - jiti diff --git a/scripts/worktree-setup.sh b/scripts/worktree-setup.sh new file mode 100755 index 00000000000..8b5740618e9 --- /dev/null +++ b/scripts/worktree-setup.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# install dependencies +direnv allow +corepack install +pnpm install + +# setup repositories +git clone --depth 1 https://github.com/effect-ts/effect-smol.git .repos/effect-v4